文章目录
- 前言
- 一、ArrayList源码摘录
- 二、测试用例
- 三、ArrayList线程不安全的表现
- 1. 并发环境下进行add操作时可能会导致elementData数组越界
- 2. 一个线程的值覆盖另一个线程添加的值
- 2.1 情况1 size大小符合预期,但是中间有null值存在
- 2.2 情况2 size大小比预期的小
- 总结
- Tips
前言
我们都知道ArrayList是线程不安全的,那么它不安全在哪里?又会出现什么并发问题呢?
提示:以下是本篇文章正文内容。
一、ArrayList源码摘录
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
transient Object[] elementData;
private int size;
}
add() 方法
public boolean add(E e) {
//确保容量足够,如果不够进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//将e存在index为size的位置(即最后一位的下一位置),size++
//我们都知道,++操作不是原子指令,多线程情况下将发生并发问题
elementData[size++] = e;
return true;
}
二、测试用例
@Test
public void listThreadUnsafe() throws InterruptedException {
List<String> list = new ArrayList<>();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
list.add("t1-" + i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
list.add("t2-" + i);
}
}
});
t1.start();
t2.start();
Thread.sleep(2000);
int size = list.size();
System.out.println("size = " + size);
for (int i = 0; i < size; i++) {
System.out.println("索引为" + i + "的元素为:" + list.get(i));
}
}
本用例多跑几次,将出现下面几种并发问题。
三、ArrayList线程不安全的表现
add()实际执行的过程为:
elementData[size] = e;
size = size + 1;
1. 并发环境下进行add操作时可能会导致elementData数组越界
问题现场如下:
有两个线程:t1,t2。有ArrayList size=9(即其中有9个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为9,调用ensureCapacityInternal()方法判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为9,也调用ensureCapacityInternal()方法判断容量是否需要扩容
t1发现自己的需求为size+1=10,容量足够,无需扩容
t1发现自己的需求为也size+1=10,容量足够,无需扩容
t1开始设置元素操作,elementData[size++] = e,成功,此时size变为10
t2也开始进行设置元素操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常:ArrayIndexOutOfBoundsException
用例体现为:
2. 一个线程的值覆盖另一个线程添加的值
这个问题要分多钟情况了
2.1 情况1 size大小符合预期,但是中间有null值存在
流程描述如下:
问题现场如下:
有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为5,调用ensureCapacityInternal()方法判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为5,也调用ensureCapacityInternal()方法判断容量是否需要扩容
t1发现自己的需求为size+1=6,容量足够,无需扩容
t1发现自己的需求为也size+1=6,容量足够,无需扩容
t1开始设置元素操作,elementData[size] = e,成功,
t2也开始设置元素操作,elementData[size] = e,成功,注意此时t1的size+1还没执行
t1 size = size + 1 = 6,并写入主存
t2 size = size + 1 = 7
这样,size符合预期,但是t2设置的值被覆盖,而且索引为6的位置将永远为null,因为size已经为7,下次add()也会从7开始。除非手动set值。
用例体现如下:
我们发现,t2的“t2-0”元素被覆盖。
2.2 情况2 size大小比预期的小
情况分析:
问题现场如下:
有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为5,调用ensureCapacityInternal()方法判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为5,也调用ensureCapacityInternal()方法判断容量是否需要扩容
t1发现自己的需求为size+1=6,容量足够,无需扩容
t1发现自己的需求为也size+1=6,容量足够,无需扩容
t1开始设置元素操作,elementData[size] = e,成功,
t2也开始设置元素操作,elementData[size] = e,成功,注意此时t1的size+1还没执行
t1 size = size + 1 = 6,暂未写入主存
t2 size = size + 1 此时因为t1操作完size还未写入主存,所以size依然为5,+1后仍为6
t1将size=6 写入主存
t2将size=6 写入主存
这样,size=6 比预期结果小了。
用例体现:
总结
上面介绍的情况都有其出现的概率,并不是每次都出现,只是在临界状态下出现错误。但是,作为程序的编写者,即使有千万分之一的概率,我们也要尽量去避免它,这是程序员的基本素养。
Tips
关于写入主存。
基本现在的CPU都是多核心的,每个核心有各自的高速缓存,计算任务需要在高速缓存中进行,对于缓存的访问速度 L1 > L2 > L3 > 内存
。L1、L2为各核心独有,L3为多个核心共享。
我们的程序运行在主内存,但计算需要在CPU中完成。
当执行计算任务时,比如 size+1
操作,CPU先将 size
的值读进CPU缓存,在CPU缓存中计算 +1,然后再将结果写入主内存。