看完这篇CopyOnWriteArrayList源码解析,和阿里面试官扯了整整一个小时!
我不停奔跑只为追赶当年被寄予厚望的自己。
——利文斯顿
0 前言
我们知道 ArrayList 非线程安全,需要自己加锁或者使用 Collections.synchronizedList
包装.
从JDK1.5开始JUC里提供了使用 CopyOnWrite 机制实现的并发容器线程安全的 List - CopyOnWriteArrayList,简称 COW
1 CopyOnWrite 设计思想
1.1 基本概念
CopyOnWrite 写时复制.
一般来说就是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器复制出一个新的容器,往新的容器里添加元素,添加完元素之后,再将原容器引用指向新容器.
即一开始大家都在共享同一内容,当有人想修改该内容时,才会真地把内容copy出去形成一个新的内容然后再改,这是一种延时懒惰策略.
1.2 设计优点
可并发读 CopyOnWrite 容器,而无需加锁,因为当前容器不会添加任何元素.
所以这也是一种读写分离的思想,读写的是不同的容器.
2 继承体系
- 和 ArrayList 的继承体系类似
3 属性
- 保护所有更改器的锁
- 仅能通过getArray / setArray访问的数组
- lock 内存偏移量
4 构造方法
4.1 无参
- 创建一个空 list
4.2 有参
下面开始看源码,到底是如何实现写时复制的.
5 add(E e)
向 COW 里添加元素,是需要加锁的,否则并发写时 copy 出N个副本!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
getArray
- 获取数组.非priavte,以便也可以从CopyOnWriteArraySet类(直接组合了CopyOnWriteArrayList作为成员变量)访问
setArray
- 将引用设置到新数组
都加锁,为什么还需要拷贝数组,而不直接在原数组修改?
- volatile 修饰的是数组引用!简单的在原来数组修改几个元素的值,这种操作是无法发挥可见性的,必须通过修改数组内存地址
- 在新数组上执行 copyOf,对原数组无任何影响,只有新数组完全拷贝完成之后,外部才能访问,避免了原数组数据变动可能造成的不良影响
6 get
get(int index)
- 读指定位置元素
get(Object[] a, int index)
读时无需加锁,如果读时其它线程正在向ArrayList添加数据,读还是只会读到旧数据,因为写时并不会锁住旧的数组.
7 remove
7.1 指定索引删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
依旧三板斧:
- 加锁
- 根据删除索引的位置,进行不同策略拷贝
- 解锁
7.2 批量删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
并非直接对数组元素逐个删除,而先对数组值循环判断,将无需删除的数据放到临时数组,最后临时数组中的数据就是我们不需要删除的数据.
8 总结
CopyOnWrite 并发容器适用于读多写少的并发场景.CopyOnWrite容器有很多优点,但同时也存在问题,开发时候需要注意:
内存占用问题
写时,内存里会同时驻存两个对象的内存,旧对象和新写入对象(复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存).若这些对象占用内存较大,很可能造成频繁GC,应用响应时间也变长.
针对该问题,可通过压缩容器中元素,减少大对象的内存,或者直接不使用CopyOnWrite容器,而使用其他并发容器,如ConcurrentHashMap。
数据一致性问题
CopyOnWrite容器只能保证数据的最终一致性
,不能保证数据的实时一致性
,请酌情使用.