CopyOnWriteArrayList是一种写时复制的ArrayList,且是线程安全版本。有很多类似设计的数据结构,如CopyOnWriteArraySet。
如果想了解具体原理剖析,请看第二章原理,为什么需要拷贝?
拓展:CopyOnWriteArraySet 线程安全的set,底层使用CopyOnWriteArrayList add的时候调用addIfAbsent来保证元素无重复 和一般set不同,像hashset、treeset底层都是调用相对应map来操作
1. 简介
CopyOnWriteArrayList顾名思义写时复制列表,避免了直接读写互斥加锁,性能低下的情况。
读:直接读取不加锁。
写:拷贝一份,进行操作。多个写操作加锁互斥保证数据安全。
优缺点:
优点:减少读取锁,提高代码性能,同时保证线程安全。
缺点:
- 当内部数据大,拷贝需要很大的内存开销,可能导致OOM的发生。
- 只能保证数据最终一致性安全。
最终一致性分三种情况讨论:
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. 原理
先问个问题
为什么要用写时拷贝,直接在原数组修改不可以吗?
毕竟在写的时候都是加锁的,不存在多个线程同时修改,而且省去拷贝的内存开销和修改数据、指向新的数组时导致一致性的问题。
这个问题当初也思考了很久。写时拷贝的目的是什么,为什么要这样做?
要解决这个问题,关键是了解volatile这个关键词
volatile的作用有两点
- 线程可见性
- 避免指令重排序
这里简单说一下可见性,多个CPU内部都会有缓存提升读取效率,首先从内存读取到缓存,如果没有volatile这个关键词,其他线程修改了该属性的值,刷回内存,但是当前线程感知不到修改, 还在读取旧的缓存值,造成数据不一致的问题。所以加上volatile可以感知到变化。同时很关键的一个点是只有引用修改了才会被感知到,比如对象修改、数组对象引用修改,变量修改等。
// volatile修饰的
private transient volatile Object[] array;
前面说到因为这里是数组引用类型,所以如果单纯在原数组进行元素修改是无法被其他线程感知到的,即volatile是无法生效的,所以这边才会使用拷贝的方式来修改引用指向及时“通知”其他线程数据变更。
所以才会使用一种写时复制方法,保证数据安全性。
3. 与ReadWriteLock比较
读写锁:在写时候是不能读,在读时候也不能写,所以和CopyOnWrite是不一样的
读写锁 | 读 | 写 |
---|---|---|
读 | √ | × |
写 | × | × |
4.总结
CopyOnWriteArrayList很巧妙的使用了volatile关键词来保证线程读写安全,虽然避免了读写同时加锁性能低下的场景,但是也有占用额外内存,数据无法保证强一致性等缺点,需要根据场景使用,适合读多写少的场景。
评论