ArrayList扩容以及线程安全问题

1,252 阅读4分钟

一、ArrayList扩容

  1. 类属性 filed

图片.png 2. 三个构造方法

图片.png

图片.png

  1. 扩容过程

    ArrayList扩容是通过add()方法触发实现的,即数组满时,再次加入新元素会触发扩容

    setp1: 创建new 数组

    setp2: 通过Arrays.copyOf(elementData, newCapacity) 复制old数组元素到新数组

    setp3: add 新元素

(1) add()方法

图片.png

(2)ensureCapacityInternal()方法

图片.png

此函数传入参数为 需要使用的最小容量值,若当前列表为空的话,在传入容量与默认容量中选最大值为初始容量大小,如此可见调用无参构造后,列表的初始容量是10

(3)ensureExplicitCapacity()方法

图片.png

图片.png

modCount 来源于父类AbastractList 用于记录列表修改的次数 fail-fast机制会用到,后面详述 判断传入的容量值与当前数组容量对比,大于数组当前容量说明容量不足 需要扩容了 扩容是通过grow() 函数实现的, 主要是确定新数组大小; 新建满足条件的数组,利用Arrays.copyOf() 方法 复制数组元素到新扩容后的数组

二、线程安全问题

ArrayList 线程不安全,相对比Vector 利用synchronized 在方法上加锁实现了线程安全; 所以多线程并发编程中不建议使用ArrryList作为容器; 具体表现:

1.在多线程并发add()时可能会导致elementData数组越界异常ArrayIndexOutOfBoundsException

 public boolean add(E e) {
  	// 第一步 确定容量 不足则扩容
    ensureCapacityInternal(size + 1);
    //第二步 添加新元素
    elementData[size++] = e;
    return true;
}

列表容量为10,当前已有9个元素 即size = 9; 线程A 进入add()方法,调用 ensureCapacityInternal 进行elementData[] 容量判断; 此时线程B 也进入add()方法,调用ensureCapacityInternal进行容量判断; 线程A 获取size = 9 ,elementData.length =10,无需扩容; 线程B 获取size = 9,elementData.length = 10, 也无需扩容; 线程A 进行赋值elementData[size++] = e 即 elementData[9] = e, size ++ 后 size == 10 线程B进行赋值elementData[size++] = e 即 elementData[10] = e 由于列表最大角标是9,线程B 无法赋值 造成了角标越界异常ArrayIndexOutOfBoundsException; 问题产生的原因:多线程并发操作add() 线程B 获取size 进行扩容判断时,线程A 还未来得及将size 加1,所以未触发数组扩容。

2.线程B覆盖线程A添加的值

elementData[size++] = e 赋值操作实际上是两步 elementData[size] = e size ++ 自增运算同样是线程不安全的; 假设size=5.若线程A在5位置存放了值valueA,获得size=5,但还没来得及将size加1写入主存。 此时线程B在也在5位置存放了值valueB,也获得size=5, 而后A、B分别将size加1后写入主存,size=6,即两个线程执行两次add()后size只加了1。 这就是两个线程并发添加100个元素到列表 最后列表的大小小于200的原因

解决方案:

(1)使用Collections工具类,用Collections工具类将线程不安全的ArrayList类转换为线程安全的集合类。小体量数据的ArrayList类可以使用这种方法创建线程安全的类。

List list=Collections.synchronizedList(new ArrayList);

(2)使用CopyOnWriteArrayList类(写时复制,读写分离)

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

图片.png CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况: 1、如果写操作未完成,那么直接读取原数组的数据; 2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据; 3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

参考链接

1.www.cnblogs.com/baichunyu/p…