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