ArrayList 线程安全问题

概述

ArrayList用在多线程环境中存在线程安全问题。关键的原因就是ArrayList底层实现,在新增元素时数组索引的移动操作。

ArrayList的add()方法源码:
在这里插入图片描述
Java中 i++ 并非线程安全的,这样多个线程同时往一个ArrayList中加元素,导致元素丢失,出现空洞。那么如果想在多线程环境中使用ArrayList,有哪些保证其线程安全性的方法呢?

代码案例

public class UnsafeArrayList2 {

    public static void main(String[] args) {
        try {
            notThreadSafe();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void notThreadSafe() throws Exception{
        final List<Integer> list = new ArrayList<>();
        for(int i=0;i<4;i++){
            new Thread(()->{
                for(int j=0;j<=10000;j++){
                    list.add(new Random().nextInt(100));
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);
        System.out.println("size = "+list.size());
        for(int i=0;i<list.size();i++){
            if(null == list.get(i)){
                System.out.println("error==========");
            }
        }
        System.out.println("over===========");
    }

}

运行结果:

size = 38879
error==========
error==========
error==========
error==========
error==========
error==========
error==========
error==========
...(此处省略)

增加元素过程中较为容易出现问题的部分在于elementData[size++] = e;.
赋值的过程可以分为两个步骤
elementData[size] = e;
size++;

我们分别使用两个线程来模拟插入过程.例如有两个线程,分别加入数字1与2.
在这里插入图片描述
运行的过程如下所示:
1、线程1 赋值 element[1] = 1; 随后因为时间片用完而中断;
2、线程2 赋值 element[1] = 2; 随后因为时间片用完中断;

此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了.
3、线程1 自增 size++; (size=2)
4、线程2 自增 size++; (size=3)

此处导致了某些值为null的问题.因为原size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了.指针index指向了3.所以,导致了某些情况下值为null的情况.

数组越界情况. 我们将上方的线程运行图更新下进行演示:
在这里插入图片描述
前提条件: 当前size=2 数组长度为2.
1、线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
2、线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
3、线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++
4、线程2 因为上文判断了数组没有越界.所以进行赋值操作.但是此时的size=3了.再执行element[3]=2. 导致了数组越界了.

由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现.

解决方案–同步

既然ArrayList不是线程安全的,第一种很容易想到的方法就是使用synchronized来同步所有的ArrayList操作方法,JDK工具类为我们提供了。Collections.synchronizedList()方法其实底层也是在集合的所有方法之上加上了synchronized(默认使用的是同一个monitor对象,也可以自己指定)。

源码:
在这里插入图片描述
代码案例:

public class UnsafeArrayList3 {

    public static void main(String[] args) {
        try {
            notThreadSafe();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void notThreadSafe() throws Exception{
        final List<Integer> list = Collections.synchronizedList(new ArrayList<>());
        for(int i=0;i<4;i++){
            new Thread(()->{
                for(int j=0;j<10000;j++){
                    list.add(new Random().nextInt(100));
                }
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);
        System.out.println("size = "+list.size());
        for(int i=0;i<list.size();i++){
            if(null == list.get(i)){
                System.out.println("error==========");
            }
        }
        System.out.println("over===========");
    }
}

运行结果

size = 40000
over===========

解决方案–COW 写时拷贝

Copy On Write 也是一种重要的思想,在写少读多的场景下,为了保证集合的线程安全性,我们完全可以在当前线程中得到原始数据的一份拷贝,然后进行操作。

JDK集合框架中为我们提供了 ArrayList 的这样一个实现:CopyOnWriteArrayList。但是如果不是写少读多的场景,使用 CopyOnWriteArrayList 开销比较大,因为每次对其更新操作(add/set/remove)都会做一次数组拷贝。

CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

源码:
在这里插入图片描述读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

内存占用问题: 因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。

数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

CopyOnWriteArrayList为什么并发安全且性能比Vector好

我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

总结

在多线程环境下可以使用 Collections.synchronizedList() 或者 CopyOnWriteArrayList 来实现 ArrayList 的线程安全性。虽然 Vector(已废弃) 每个方法也都有同步关键字,但是一般不使用,一方面是慢,另一方面是不能保证多个方法的组合是线程安全的(因为不是基于同一个monitor)。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值