文章目录
前言
大家都知道ArrayList是线程不安全的,那么ArrayList为什么线程不安全呢,线程不安全的表现是什么?有什么解决方法?本篇文章带大家来探讨一下。
一、线程不安全的三种表现
演示代码:
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class ArrayListSafe {
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
for (int j = 0; j < 2; ++j) {
new Thread(() -> {
for (int i = 0; i < 100; ++i) {
data.add();
}
}).start();
}
TimeUnit.SECONDS.sleep(1);
data.print();
}
}
class Data{
ArrayList<Integer> arrayList = new ArrayList<>();
public void add(){
arrayList.add(new Random().nextInt(9));
}
public void print(){
System.out.println(arrayList.size());
for(int i=0;i<arrayList.size();++i){
System.out.println("第"+i+"个元素:"+arrayList.get(i));
}
}
}
1.空指针异常
情况一:size不达标
情况二:size达标
原因分析
为了分析以上两种情况,我们先来看一下ArrayList的add()方法:
注意:其中elementData[size++] = e;
可以拆分为:
elementData[size] = e;
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 比预期结果小了。
情况二:
代码中有两个线程,假设为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值。
2.数组越界异常
分析:
还是从add()方法下手:
代码中有两个线程,假设为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
3.并发修改异常
演示代码(注释的地方修改):
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class ArrayListSafe {
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
for (int j = 0; j < 2; ++j) {
new Thread(() -> {
for (int i = 0; i < 100; ++i) {
data.add();
}
}).start();
}
TimeUnit.SECONDS.sleep(1);
data.print();
}
}
class Data{
ArrayList<Integer> arrayList = new ArrayList<>();
public void add(){
arrayList.add(new Random().nextInt(9));
//每次添加完元素后输出目前数组的情况
System.out.println(arrayList);
}
public void print(){
System.out.println(arrayList.size());
for(int i=0;i<arrayList.size();++i){
System.out.println("第"+i+"个元素:"+arrayList.get(i));
}
}
}
分析:
通过错误信息我们可以直接定位到发生异常的地方:
由于预期的modCount不等于expectModCount,所以抛出错误
假设代码中有两个线程,假设为t1和t2
t1在添加完5个元素后,就开始遍历当前的数组,此时modCount等于5,还没遍历完的时候
t2紧接着又对数组进行了add操作,导致modCount加1
t1在判断当前modCount跟exceptmodCount发现不相等
抛出异常
这个异常要讲起来篇幅较长,感兴趣的朋友可以看一下这篇博文:
ConcurrentModificationException异常
二、解决方法
1.将ArrayList替换成Vector
Vector<Integer> arrayList = new Vector<>();
2.Collections.synchronizedList()
List<Integer> arrayList = Collections.synchronizedList(new ArrayList<>());
3.使用CopyOnWriteArrayList
List<Integer> arrayList1 = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理是读写分离,其对写操作使用ReentrantLock来上锁,对读操作则不加锁;CopyOnWriteArrayList在写操作的时候,会将list中的数组拷贝一份副本,然后对其副本进行操作(如果此时其他线程需要读的事,那么其他线程读取的是原先的没有修改的数组,如果其他写操作的线程要进行写操作,需要等待正在写的线程操作完成,释放ReentrantLock后,去获取锁才能进行写操作),写操作完成后,会讲list中数组的地址引用指向修改后的新数组地址。
总结
1、本文介绍了ArrayList在多线程的情况下可能会出现的三种异常,并分析了原因,结尾给出了三种解决ArrayList线程不安全的方案,一和二两种方法都是将所有的方法都加锁,那会导致效率低下,只能一个线程操作完,下一个线程获取到锁才能操作。
2、CopyOnWriteArrayList由于写时进行复制,内存里面同时存在两个对象占用内存,如果对象过大容易发送YongGc和FullGc,如果使用场景的写操作十分频繁的话,建议还是不要实现CopyOnWriteArrayList。