CopyOnWriteArrayList写时复制的剖析

CopyOnWriteArrayList是一种写时复制的ArrayList,且是线程安全版本。有很多类似设计的数据结构,如CopyOnWriteArraySet。

如果想了解具体原理剖析,请看第二章原理,为什么需要拷贝?

拓展:CopyOnWriteArraySet 线程安全的set,底层使用CopyOnWriteArrayList add的时候调用addIfAbsent来保证元素无重复 和一般set不同,像hashset、treeset底层都是调用相对应map来操作

1. 简介

CopyOnWriteArrayList顾名思义写时复制列表,避免了直接读写互斥加锁,性能低下的情况。

读:直接读取不加锁。

写:拷贝一份,进行操作。多个写操作加锁互斥保证数据安全。

优缺点

优点:减少读取锁,提高代码性能,同时保证线程安全。

缺点:

  1. 当内部数据大,拷贝需要很大的内存开销,可能导致OOM的发生。
  2. 只能保证数据最终一致性安全。

最终一致性分三种情况讨论:

public boolean add(E e) {
    synchronized(this.lock) {
        Object[] es = this.getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        // 1️⃣
        es[len] = e;
        // 2️⃣
        this.setArray(es);
        // 3️⃣
        return true;
    }
}
  • 1️⃣未修改数据

    这个时候读取旧的数据

  • 2️⃣修改数据,但未将引用指向新的数组

    这个时候也会读取到旧的数据。

  • 3️⃣修改数据,将引用指向新的数据

    这个时候能读取到最新的数据。

2. 原理

先问个问题

  1. 为什么要用写时拷贝,直接在原数组修改不可以吗?

    毕竟在写的时候都是加锁的,不存在多个线程同时修改,而且省去拷贝的内存开销和修改数据、指向新的数组时导致一致性的问题。

    这个问题当初也思考了很久。写时拷贝的目的是什么,为什么要这样做?

要解决这个问题,关键是了解volatile这个关键词

volatile的作用有两点

  1. 线程可见性
  2. 避免指令重排序

这里简单说一下可见性,多个CPU内部都会有缓存提升读取效率,首先从内存读取到缓存,如果没有volatile这个关键词,其他线程修改了该属性的值,刷回内存,但是当前线程感知不到修改, 还在读取旧的缓存值,造成数据不一致的问题。所以加上volatile可以感知到变化。同时很关键的一个点是只有引用修改了才会被感知到,比如对象修改、数组对象引用修改,变量修改等。

// volatile修饰的
private transient volatile Object[] array;

前面说到因为这里是数组引用类型,所以如果单纯在原数组进行元素修改是无法被其他线程感知到的,即volatile是无法生效的,所以这边才会使用拷贝的方式来修改引用指向及时“通知”其他线程数据变更。

所以才会使用一种写时复制方法,保证数据安全性。

3. 与ReadWriteLock比较

读写锁:在写时候是不能读,在读时候也不能写,所以和CopyOnWrite是不一样的

读写锁
×
××

4.总结

CopyOnWriteArrayList很巧妙的使用了volatile关键词来保证线程读写安全,虽然避免了读写同时加锁性能低下的场景,但是也有占用额外内存,数据无法保证强一致性等缺点,需要根据场景使用,适合读多写少的场景。

end
  • 作者:Endwas(联系作者)
  • 发表时间:2022-01-26 11:39
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转博主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者名字和博客地址
  • 评论