1、ArrayList线程不安全的三种结果
(1)某些信息没有添加成功(丢失了一些数据)
(2)下标越界
(3)Null
分析
1)丢失数据
先看ArrayList.add()的源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以得出ArrayList添加数据大体分为2步,第一步确定数组长度是否满足即需不需要扩容,第二步添加数据
这里假设2个线程,线程A,线程B
线程A:获取当前size为2,确定范围不需要扩容,执行 elementData[size++] = e;这里分为2大步第一步elementData[size] = e,第二步size++,size++又分为三步:(1)获取size的值,(2)size+1,(3)重新赋值给size;于是出现A执行完第一大步和第二大步的第一小步之后被挂起,线程B正常执行完add操作,此时A获取到cpu资源继续运行,执行第二步size++,但此前赋予的值却被线程B覆盖了,这就是丢失数据的原因
**(
2)下标越界问题
这里主要关注add方法的第一步
在JDK1.8中,ArrayList构造一个实例是不会初始化一个长度为10的数组的,而是一个叫做DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空数组,在第一次执行add方法时会初始化一个长度为10的数组,这里简单说一下流程(以下是讲解第一次执行add()方法初始化数组的过程)
第一次执行add()方法时,给ensureCapacityInternal()传递一个size+1的参数即minCapacity = 1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
再到calculateCapacity(),这里首先判断了是否是一个为空的数组,若是则返回默认大小10和minCapacity中较大的数值
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
在执行ensureExplicitCapacity(int minCapacity)这里的参数是ensureCapacityInternal()的返回值即10,modCount是记录修改的次数,判断minCapacity和当前数组的长度,若比当前数组大则需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
再看grow(),首先定义一个newCapacity用于接收扩容后的长度,oldCapacity+(oldCapacity >> 1)等价于1.5倍的oldCapacity,因为oldCapacity为0,所以扩容后的值仍小于minCapacity=10,于是
newCapacity = minCapacity,创建了一个长度为newCapacity长度的数组,并由elementData指向。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
下面讲一下多线程临界扩容存在的问题,假设2个线程A、B,当前的size为9。
线程A需要添加一个数,于是对size+1进行判定,发现并没有超过10于是不执行grow命令,此时A被挂起,线程B也要添加一个数据,对size+1进行判定,此时size仍等于9于是也不执行grow命令,并成功运行在size=10的位置添加了一个值,此时A开始继续运行,此时size已经等于10了,运行到elementData[size++] = e时就会出现下标越界的问题
(图是偷得)
3)Null
其实和第一个数据丢失非常类似,因为size++分为三步(1)获取size的值,(2)size+1,(3)重新赋值给size。在数据丢失的情况下为执行了第一小步(1)获取size的值之后被挂起,而出现null值是因为线程A还没到获取size的值就被挂起了,线程B完整执行完后,在继续执行线程A,此时获取的size值为线程B修改过之后的值,线程A继续size+1,就会出现null值,所以null值得出现也必定会有数据覆盖
纯个人理解,欢迎互相交流!!