在并发编程—ArrayList线程安全问题一文中提到ArrayList
是线程不安全的,可以使用CopyOnWriteArrayList
来保证线程安全。本文主要介绍一下CopyOnWriteArrayList
这个类。
源码分析
首先我们可以看下CopyOnWriteArrayList
的数据结构,通过源码可以看到CopyOnWriteArrayList
类中维护一个array对象数组用于存储集合的每个元素,并且array数组只能通过getArray和setArray方法来访问。
1 | /** The lock protecting all mutators */ |
为什么和ArrayList
相比,CopyOnWriteArrayList
是线程安全的呢?就是因为CopyOnWriteArrayList
的写操作都加了锁。看一下add(E e)
方法的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1、先加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
//5、解锁
return true;
} finally {
lock.unlock();
}
}
可以看到调用add方法的时候,也是通过getArray()获取到对象数组,再直接新生成一个数组,最后把旧的数组的值复制到新的数组中,然后直接使用新的数组覆盖实例变量array。这也是它名字的由来:写时复制。
同样的在删除CopyOnWriteArrayList
里的元素时,也同样会有一个这样复制的过程。
下图演示了两个线程并发读写CopyOnWriteArrayList
的情况,其中线程1需要通过iterator()方法读取数据,线程2往集合中添加一个元素:元素3,线程2的操作是直接基于集合当前的数据进行复制一份到新的一个数组,最后将array变量指向新的一个数组。
iterator()
方法本质上就是遍历数组array:1
2
3public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
注意思考这样的一个场景:假设线程1在遍历元素的时候,拿到的是复制之前的数组,里面包含了元素1和元素2,于此同时线程2复制出一个新数组并添加了元素3,这个时候线程1是无法读取到元素3的。这也是CopyOnWriteArrayList的一个特点:弱一致性。意思就是说线程1看到的是某一时刻的一份“快照数据”,无法保证能读取到最新的数据。
总结
CopyOnWriteArrayList
通过加锁的方式解决了ArrayList
并发的线程安全问题,通过弱一致性提升读请求并发,适合用在数据读多写少的场景,比如数据库配置这种,基本已读取为主。但它也存在一些缺点:
- 有了锁的额外开销。
- 每次写操作都要进行拷贝,比较消耗性能和内存。
- 无法保证数据额实时一致性。
CopyOnWriteArrayList透露的思想
分析CopyOnWriteArrayList表达的一些思想:
- 读写分离,读和写分开
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突