面试官:你来给我讲一下ArrayList线程不安全的几种表现,怎么解决?


前言

大家都知道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。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值