05容器篇(D2_集合 - D6_容器源码分析篇 - D1_ArrayList)

news2025/1/8 5:27:25

目录

本章目标

一、基本介绍

二、原理分析

1. 数据结构源码分析

2. 默认容量&最大容量

为什么最大容量要-8呢?

3. 为什么ArrayList查询快,增删慢?

4. 初始化容量

1> 创建ArrayList对象分析:无参数

2> 创建ArrayList对象分析:带有初始化容量构造方法

5. 扩容原理

6. 知识小结

三、线程安全问题及解决方案

1. 错误复现

2. 导致ArrayList线程不安全的源码分析

3. 解决方案

四、Fail-Fast机制深入理解

1. 什么是Fail-Fast机制?

2. Fast-Fail事件复现及解决方案


本章目标

  • 理解ArrayList的底层数据结构
  • 深入掌握ArrayList查询快,增删慢的原因
  • 掌握ArrayList的扩容机制
  • 掌握ArrayList初始化容量过程
  • 掌握ArrayList出现线程安全问题原因及解决方案
  • 掌握ArrayList的Fail-Fast机制

一、基本介绍

ArrayList集合是Collection和List接口的实现类。底层的数据结构是数组。数据结构特点 : 增删慢,查询 快。

线程不安全的集合!

许多程序员开发的时候,使用集合基本上无脑选取ArrayList!不建议这种用法。

ArrayList的特点:

  • 单列集合 : 对应与Map集合来说【双列集合】
  • 有序性 : 存入的元素和取出的元素是顺序是一样的
  • 元素可以重复 : 可以存入两个相同的元素
  • 含带索引的方法 : 数组与生俱来含有索引【下角标】

二、原理分析

1. 数据结构源码分析

//空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量空对象数组,通过空的构造参数生成ArrayList对象实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList对象的实际对象数组!
transient Object[] elementData; // non-private to simplify nested class access
//1、为什么是Object类型呢?利用面向对象的多态特性,当前ArrayList的可以存储任意引用数据类型。
//2、ArrayList有一个问题,不能存储基本数据类型!就是数组的类型是Object类型

2. 默认容量&最大容量

//默认的初始化容量是10
private static final int DEFAULT_CAPACITY = 10;
//最大容量 : 2^31 - 1 - 8 = 21 4748 3639【21亿】
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

为什么最大容量要-8呢?

目的是为了存储ArrayList集合的基本信息,比如list集合的最大容量!

3. 为什么ArrayList查询快,增删慢?

ArrayList的底层数据结构就是一个Object数组,一个可变的数组,对于其的所有操作都是通过数组来实现的。

  • 数组是一种,查询快、增删慢! 查询数据是通过索引定位,查询任意数据耗时均相同。查询效率贼高!
  • 删除数据时,要将原始数据删除,同时后面的每个数据迁移。删除效率就比较低!
  • 新增数据,在添加数组的位置加入数组,同时在数组后面位置后移以为!添加效率极低!

4. 初始化容量

ArrayList底层是数组,动态数组!

  • 底层是Object对象数组,数组存储的数据类型是Object,数组名字为elementData。
transient Object[] elementData;

1> 创建ArrayList对象分析:无参数

创建ArrayList的之后,ArrayList容量是多少呢?回答10是错误的!回答0是正确【限定条件,在JDK1.8中】

如何初始化 动态数组的容量?10个

构造方法

/**
 * Constructs an empty list with an initial capacity of ten.
 */
//初始化的ArrayList的容量,是10个!
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//空数组!
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

在执行add()方法的时候初始化!【懒加载】

判断当前数组的容量是否有存储空间,如果没有初始化一个10的容量。

//想数组中,添加一个元素
public boolean add(E e) {
    //确保有容量,如果第一次添加,会初始化一个容量为10的list
    //size当前集合元素的个数,随着添加的元素递增
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //添加元素
    elementData[size++] = e;
    return true;
}

//ensureCapacityInternal确保有容量,如果第一次添加,会初始化一个容量为10的list
private void ensureCapacityInternal(int minCapacity) {
    //两个方法
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// calculateCapacity(elementData, minCapacity) 拿着当前ArrayList的数组,与当前数组中的元素个数。计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //ArrayList的数组 与默认的数组进行比较。、
    //{} == {}
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {/true
        //DEFAULT_CAPACITY = 10
        //minCapacity 1
        //1和10比谁大 10
        return Math.max(DEFAULT_CAPACITY, minCapacity);//计算之后,返回的初始化容量是10
   }
    return minCapacity;
}

// ensureExplicitCapacity() 确保不会超过数组的真实容量
private void ensureExplicitCapacity(int minCapacity) {
    //minCapacity 当前计算后容量 10
    modCount++;//对当前数组操作计数器
    // overflow-conscious code
    //最小的容量 : 10 - 当前数组的容量{} 0
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);//做了扩容
}

2> 创建ArrayList对象分析:带有初始化容量构造方法

//创建ArrayList集合,并且设置固定的集合容量
public ArrayList(int initialCapacity) {
    //initialCapacity 手动设置的初始化容量
    if (initialCapacity > 0) {//判断容量是否大于0,如果大于0
        //创建一个对象数组位指定容量大小,并且交给ArrayList对象
        this.elementData = new Object[initialCapacity];
        //如果设置的容量为0,设置默认数组
   } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;//默认的元素数据数组{}
   } else {
        //如果不是0,也不是大于0的数,会抛出非法参数异常!
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
   }
}

注意 : 使用ArrayList的集合,建议如果知道集合的大小,最好提前设置。提示集合的使用效率!

5. 扩容原理

add方法先要确保数组的容量足够,防止数组已经填满还往里面添加数据造成数组越界:

  1. 如果数组空间足够,直接将数据添加到数组中
  2. 如果数组空间不够了,则进行扩容。扩容1.5倍扩容。
  3. 扩容 : 原始数组copy新数组中,同时向新数组后面加入数据

注意 : new的ArrayList的对象没有容量的,在第一次添加的add,会进行第一次扩容。0 -> 10!

//grow扩容数组
private void grow(int minCapacity) {
    //minCapacity 当前数组的最小容量,存储了多少个元素
    // overflow-conscious code
    //获取当前存储数据数组的长度
    int oldCapacity = elementData.length;
    //新的容量 = 旧的容量 + 扩容的容量【旧容量/2 = 0.5旧容量】
    //扩容1.5倍扩容
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //极端情况过滤 : 新的容量 - 旧的容量小于0【int值移除】
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;//不扩容了
    //新的容量,比ArrayList的最大值,还要打
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //设置新的容量为ArrayList的最大值,以ArrayList最大值为当前容量
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

6. 知识小结

  1. 扩容的规则并不是翻倍,是原来容量的1.5倍
  2. ArrayList的数组最大值Integer.MAX_VALUE。不允许超过这个最大值
  3. 新增元素时,没有严格的数据值的检查。所有可用设置null

三、线程安全问题及解决方案

1. 错误复现

ArrayList 我们都知道底层是以数组方式实现的,实现了可变大小的数组,它允许所有元素,包括null。

看下面一个例子:开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素

/**
 * 目标 : 线程安全问题复现
 */
public class Demo04 {
    //全局线程共享集合ArrayList
    protected static ArrayList<Object> arrayList = new ArrayList<>();
    public static void main(String[] args) {
        //1.创建线程数组【500】
        Thread[] threads = new Thread[500];
        //2.遍历数组,想线程中添加500线程对象
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new MyThread();
            threads[i].start();//启动线程
       }
        //3.遍历线程,等待线程执行完毕【等待所有线程执行完毕】
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();//等待线程执行完毕
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
       }
        //线程执行内容 : 向集合中添加自己的线程名称
        //4.遍历list集合,获取所有线程的名称
        for (Object threadName : arrayList) {
            System.out.println("threadName = " + threadName);
       }
   }
}

//线程执行内容,是想集合中添加自己的线程名称
class MyThread extends Thread {
    @Override
    public void run() {
        try {
            //线程休眠1000
            Thread.sleep(1000);
            //向集合中添加自己的线程名称【操作共享内容,会出现线程安全问题】
            Demo04.arrayList.add(Thread.currentThread().getName());
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
   }
}

运行代码结果可知,会出现以下几种情况:

  • ① 打印null
  • ② 某些线程并未打印
  • ③ 数组角标越界异常

2. 导致ArrayList线程不安全的源码分析

ArrayList成员变量

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    //ArrayList的Object的数组存所有元素。
    transient Object[] elementData; // non-private to simplify nested class access
    //size变量保存当前数组中元素个数。
    private int size;
    //...
}
  • ArrayList的Object的数组存所有元素。
  • size变量保存当前数组中元素个数。

出现线程不安全源码之一 : add()方法

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

add添加元素,实际做了两个大的步骤:

  1. 判断elementData数组容量是否满足需求
  2. 在elementData对应位置上设置值

线程不安全的隐患【1】,导致③数组下标越界异常

线程不安全的隐患【2】,导致①Null、②某些线程并未打印

由此我们可以得出,在多线程情况下操作ArrayList 并不是线性安全的。

那如何解决呢?

3. 解决方案

第一种方案:使用Vector集合,Vector集合是线程安全的

//线程安全问题解决方案1
protected static Vector<Object> vector = new Vector<>();

第二种方案:使用Collections.synchronizedList。它会自动将我们的list方法进行改变,最后返回给我们 一个加锁了List

//线程安全问题解决方案2
//将集合改为同步集合
protected static List<Object> synList = Collections.synchronizedList(arrayList);

第三种方案:使用JUC中的CopyOnWriteArrayList类进行替换。【】

//线程安全问题解决方案3 JUC 【最佳选择】
protected static CopyOnWriteArrayList<Object> copyOnWriteArrayList = new CopyOnWriteArrayList<>();

四、Fail-Fast机制深入理解

1. 什么是Fail-Fast机制?

"快速失败"即Fail-Fast机制,它是Java中一种错误检测机制!

当多钱程对集合进行结构上的改变,或者在迭代元素时直接调用自身方法改变集合结构而没有通知迭代 器时,

有可能会触发Fail-Fast机制并抛出异常【ConcurrentModificationException】。注意,是有可能 触发Fail-

Fast,而不是肯定!

触发时机 : 在迭代过程中,集合的结构发生改变,而此时迭代器并不知情,或者还没来得及反应,便会 产生Fail-

Fast事件。

再次强调,迭代器的快速失败行为无法得到保证!一般来说,不可能对是否出现不同步并发修改,或者 自身修改

做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。

Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;快 速失败的

迭代器抛出ConcurrentModificationException,而安全失败的迭代器从不抛出这个异常。

上代码 :

2. Fast-Fail事件复现及解决方案

/**
 * 目标 : 复现Fast_Fail机制
 * 1.产生条件 :
 *   当多线程操作同一个集合
 *   同时遍历这个集合,该集合被修改!
 * 2.解决方案 :使用并发编程包中的集合,替换愿有集合CopyOnWriteArrayList
 */

public class Demo06 {
    //定义全局共享集合 :
    //static ArrayList<String> list = new ArrayList<>();
    //Fast_Fail机制CopyOnWriteArrayList
    static CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
    public static void main(String[] args) {
        //创建线程1,并且向集合添加元素,打印集合中的内容
        Thread thread1 = new Thread(() -> {
            //并且向集合添加元素
            for (int i = 0; i < 6; i++) {
                copyOnWriteArrayList.add("" + i);
                // 打印集合中的内容
                printAll();
           }
       });
        thread1.start();//启动线程1
        //创建线程2,并且向集合添加元素,打印集合中的内容
        Thread thread2 = new Thread(() -> {
            //并且向集合添加元素
            for (int i = 10; i < 16; i++) {
                copyOnWriteArrayList.add("" + i);
                // 打印集合中的内容
                printAll();
           }
       });
        thread2.start();//启动线程2
   }
    /**
     * 使用迭代器打印集合
     */
    public static void printAll() {
        //获取当前集合的迭代器
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        //通过迭代器遍历集合
        while (iterator.hasNext()) {
            String value = iterator.next();
            System.out.println(value + ",");
       }
   }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2273019.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TVbox 手机、智能电视节目一网打尽

文章目录 一、简要介绍二、下载地址 一、简要介绍 TVbox是目前最火爆的多端、多源的电视影音工具&#xff0c;是一款开源的自定义添加站源的影音工具。TVBox&#xff0c;支持电视频道直播。一款TV端影视工具&#xff0c;软件本身不具有任何影视资源&#xff0c;但可以通过配置…

IP Anycast 与 CDN

基于名字寻址而不是基于地址寻址早就不是什么新鲜事&#xff0c;我们日常生活中的寻址基本都是找名字&#xff0c;比如找厕所&#xff0c;找连锁店&#xff0c;我们倾向于去具有同样称呼的最近那家。IP 网络中的这种机制叫 Anycast。 是不是一下子就不需要过多解释了。所有具有…

【0x006D】HCI_Write_LE_Host_Support命令详解

目录 一、命令概述 二、命令格式及参数说明 2.1. HCI_Write_LE_Host_Support命令格式 2.2. LE_Supported_Host 三、生成事件及参数 3.1. HCI_Command_Complete 事件 3.2. Status 四、命令执行流程 4.1. 命令发起阶段&#xff08;主机端&#xff09; 4.2. 命令处理阶段…

Harmony OS开发之ArkUI框架速成九弹性布局和层叠布局

> 程序员Feri一名12年的程序员,做过开发带过团队创过业,擅长Java相关开发、鸿蒙开发、人工智能等,专注于程序员搞钱那点儿事,希望在搞钱的路上有你相伴&#xff01;君志所向,一往无前&#xff01; --- 1.弹性布局&#xff08;Flex&#xff09; 弹性布局分为单行布局和多行…

HarmonyOS-面试资料

1. HarmonyOS-面试资料 1.1. HarmonyOS 优点、特点 1.1.1. 优点 &#xff08;1&#xff09;在国家方面&#xff0c;是国产的系统&#xff0c;受国家支持不会有限制的情况。   &#xff08;2&#xff09;设备互连18N(1:手机 8&#xff1a;平板、PC、vr设备、可穿戴设备、智慧…

macos安装java8

下载 dmg方式安装 安装 双击pkg运行 输入java -version验证 配置环境变量 cd ~ ls -a输入 ls -a后查看是否已经存在.bash_profile文件&#xff0c;如果已经存在就不需要创建&#xff0c;如果不存在&#xff0c;继续执行下方命令创建文件 touch .bash_profile /usr/l…

记一次k8s下容器启动失败,容器无日志问题排查

问题 背景 本地开发时&#xff0c;某应用增加logback-spring.xml配置文件&#xff0c;加入必要的依赖&#xff1a; <dependency><groupId>net.logstash.logback</groupId><artifactId>logstash-logback-encoder</artifactId><version>8…

KAFKA入门:原理架构解析

文章目录 一、认识kafka二、架构介绍2.1 工作流程2.2 Kafka可靠性保证2.3 Kafka存储 一、认识kafka Kafka到底是个啥&#xff1f;用来干嘛的&#xff1f; 官方定义如下&#xff1a; Kafka is used for building real-time data pipelines and streaming apps. It is horizont…

Redis - 6 ( 9000 字 Redis 入门级教程 )

一&#xff1a;Redis Java 集成到 Spring Boot 1.1 使用 Spring Boot 连接 Redis 单机 在创建项目时&#xff0c;勾选 NoSQL 分类下的 Spring Data Redis&#xff0c;同时勾选 Web 分类下的 Spring Web。这样既能方便集成 Redis&#xff0c;又能通过 Web 接口进行后续测试&am…

笔记本如何录屏幕视频和声音?快速入门的两种方法

“你好&#xff01;我想要制作线上教学课程&#xff0c;包括录制课程内容和我的声音&#xff0c;然后分享给我的学生&#xff0c;以便他们课后复习&#xff0c;但我不知道笔记本如何录屏幕视频和声音&#xff1f;有没有好的工具推荐&#xff1f;” 随着远程办公、在线学习和直播…

Flutter中的网络请求图片存储为缓存,与定制删除本地缓存

Flutter中的网络请求图片存储为缓存&#xff0c;与定制删除本地缓存 1&#xff1a;封装请求图片函数 2&#xff1a;访问的图片都会转为本地缓存&#xff0c;当相同的请求url&#xff0c;会在本地调用图片 3&#xff1a;本地缓存管理【windows与andriod已经测试】【有页面】【有…

Android设备使用AOA协议进行主机与配件模式通信

1.使用TYPC-C数据线连接两台华为手机&#xff1a; TYPE-C线&#xff0c;先连接下图右边的ACCESSORY 再连接左边的HOST 此时左边的HOST(白色) 会给右边的ACCESSORY(黑色) 充电 接着打开左连接的HostChart会自动调起授权&#xff0c;然后会启动右边的AccessoryChart USB HOS…

机器学习基础-支持向量机SVM

目录 基本概念和定义 1. 超平面&#xff08;Hyperplane&#xff09; 2. 支持向量&#xff08;Support Vectors&#xff09; 3. 线性可分 4. 边界 SVM算法基本思想和分类 基本思想 间隔最大化 间隔&#xff08;Margin&#xff09; 软边距 SVM 核函数的概念 基本概念…

ubuntu开机启动服务

需求背景&#xff1a; 需要监控日志&#xff0c;每次都是手动启动 nohup ./prometheus >/dev/null & nohub ./node_exporter >/dev/null & 需求目标&#xff1a; 重启后系统自动启动服务

图漾相机基础操作

1.客户端概述 1.1 简介 PercipioViewer是图漾基于Percipio Camport SDK开发的一款看图软件&#xff0c;可实时预览相机输出的深度图、彩色图、IR红外图和点云图,并保存对应数据&#xff0c;还支持查看设备基础信息&#xff0c;在线修改gain、曝光等各种调节相机成像的参数功能…

【计算机网络】课程 实验二 交换机基本配置和VLAN 间路由实现

实验二 交换机基本配置和VLAN 间路由实现 一、实验目的 1&#xff0e;了解交换机的管理方式。 2&#xff0e;掌握通过Console接口对交换机进行配置的方法。 3&#xff0e;掌握交换机命令行各种模式的区别&#xff0c;能够使用各种帮助信息以及命令进行基本的配置。 4&…

【论文笔记】QLoRA: Efficient Finetuning of Quantized LLMs

&#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平。 基本信息 标题: QLoRA: Efficient Finetun…

Apache Paimon-实时数据湖

一、Apache Paimon是什么? Flink社区希望能够将 Flink 的 Streaming 实时计算能力和 Lakehouse 新架构优势进一步结合&#xff0c;推出新一代的 Streaming Lakehouse 技术&#xff0c;促进数据在数据湖上真正实时流动起来&#xff0c;并为用户提供实时离线一体化的开发体验。 …

为什么相关性不是因果关系?人工智能中的因果推理探秘

目录 一、背景 &#xff08;一&#xff09;聚焦当下人工智能 &#xff08;二&#xff09;基于关联框架的人工智能 &#xff08;三&#xff09;基于因果框架的人工智能 二、因果推理的基本理论 &#xff08;一&#xff09;因果推理基本范式&#xff1a;因果模型&#xff0…

AI Development Notes 1 - introduction with the OpenAI API Development

Official document&#xff1a;https://platform.openai.com/docs/api-reference/chat/create 1. Use APIfox to call APIs 2.Use PyCharm to call APIs 2.1-1 WIN OS.Configure the Enviorment variable #HK代理环境&#xff0c;不需要科学上网(价格便宜、有安全风险&#…