目录
前言
ArrayList是使用较为频繁的一种数据结构,其底层是基于动态数组实现,具有快速随机访问的优点。而ArrayList添加元素以及扩容并不是原子的,因此在高并发的情况下并不能保证数据的正确性和一致性。(以下代码基于JDK23版本,与JDK1.8版本部分不一致,是因为JDK10之后扩容机制做了一些调整。)
一、ArrayList扩容
一、添加元素流程
private static final int DEFAULT_CAPACITY = 10;
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
从上述源码来看,ArrayList添加元素先判断当前数组的长度是否能够放入,当数组长度足够则直接放入,且size值加1。当数组长度不足以放入新元素,则会进行扩容。扩容时传入的三个参数分别是旧数组的容量、所需的最小容量与旧数组容量的差值以及旧容量的一半。
二、是扩容当前容量的一半还是所需的最小容量?
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// preconditions not checked because of inlining
// assert oldLength >= 0
// assert minGrowth > 0
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
扩容长度由所需最小容量与当前容量一半共同决定,取其中的最大值。
二、ArrayList为什么是线程不安全的?
整体上ArrayList添加一个元素的流程为:
- 判断是否需要扩容,如果需要则先扩容
- 添加元素
- size值加1
由于这些操作并不是原子的,在高并发下会受到其他线程的影响,会造成数据混乱问题。
三、ArrayList高并发下会出现什么问题?
1.会造成数据丢失:当线程1在索引为5的位置添加新的元素,由于小于初始容量10不需要扩容,所以直接添加元素,但此时size的值还未加1。线程2进行也进行添加元素,而此时的size依旧为5,会将线程1中添加的element[5]中的元素进行覆盖,造成了数据的丢失。同时,由于会size会进行两次加1,下一次元素则会添加在element[7],所以会造成element[6]中的值为null。
2.索引越界异常:当线程1在size为9的位置添加元素,此时不需要扩容。而线程2此时也开始添加元素,由于线程1的size还未加1,因此线程2依旧是添加在size为9的位置上。最终size加两次后,此时的size为11,下次进行扩容判断时会因为(11 == 10)的条件为false,导致不会进行扩容操作,此时会将元素添加在element[11],造成索引越界异常
3.size值与add的次数不一致:由于size+1的操作不是原子的,该操作可以分为三步:获取size值、size值加1、赋值给size。因此会出现线程1与线程2拿到的是同一个值,加完后再赋值会出现只加1次,而本应该加两次,造成size值与实际不一致的情况。例如此时size值为5,线程1与线程2读取的都是5,两者都对size加1并赋值,此时size值为6,而实际size值需要加2次,也就是size最终结果应该为7。
四、线程安全的ArrayList
因此,当我们需要保证线程安全的情况下,可以使用CopyOnWriteArrayList来代替ArrayList。
总结
本文从源码的角度解释了ArrayList添加元素的流程,以及简述了扩容机制。并分析了高并发下ArrayList添加元素可能会出现数据丢失、索引越界异常、size值与add的次数不一致等问题。