写时复制集合 —— CopyOnWriteArrayList
前言
JUC 下面还有一个系列的类,都是 CopyOnWriteXXX ,意思是写时复制,这个究竟是怎么回事?那就以 CopyOnWriteArrayList 为切入点,一起了解写时复制是怎么回事?
公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!
介绍
ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
像名字一样,每次进行操作的时候,都会进行一次复制,当然会有很大的性能消耗,但是在某些使用场景下,又会提高性能。具体是怎么操作的,那就一步一步阅读源码,然后再做总结归纳。
基本使用
问题疑问
为什么要叫写时复制集合?
CopyOnWriteArrayList 实现原理是什么?
CopyOnWriteArrayList 和 ArrayList 有什么区别?
CopyOnWriteArrayList 复制是怎么进行复制的?
源码分析
基本结构
参数介绍
通过参数可以了解到以下内容:
基于数组实现;
使用了 ReentrantLock 互斥锁。
构造函数
在初始化 CopyOnWriteArrayList 时,就是创建了一个 Object 的数组。
add
add 方法逻辑很简单:
通过加互斥锁(ReentrantLock)从而保证在写的时候只有一个线程可以写。
新增元素时,先使用
Arrays.copyOf(elements, len + 1)
复制出一个长度 +1 的新数组。添加元素到新数组。
然后再将原数组对象指向新数组。
画图如下:
remove
remove 方法相对多了一些判断:
通过加互斥锁(ReentrantLock)从而保证在写的时候只有一个线程可以移除元素。
如果移除的是最后一个元素,则直接复制前面的元素到新数组,并指向新数组即可。
如果移除的是中间的元素,则需要进行两次复制,然后指向新数组。
画图如下:
get
get 方法可以看出:
获取元素并没有进行加锁。
从原数组获取的元素。
所以并发情况下,并不能保证很及时的读取的刚插入或者移除的元素。
数组复制
通过阅读 add 和 remove 相关代码,可以看到在数组复制时使用了 Arrays.copyOf
和 System.arraycopy
,这相当于一个优化方面吧。
毕竟数组复制总不能把原数组遍历一遍,挨着赋值到新数组里面吧。
那接下来看一下内部是如何实现的:
通过阅读 Arrays.copyOf
相关源码,发现其实 Arrays.copyOf
底层也是调用的 System.arraycopy
可以看到 System.arraycopy
是一个 native
方法,这个是 JVM 内部实现的,具体可以阅读相关资料。而使用这种方式要比 for
循环和 clone
要高效很多。
总结
Q&A
Q: 为什么要叫写时复制集合?
A: 因为在 add、remove 操作时会复制出来一个新数组。
Q: CopyOnWriteArrayList 实现原理是什么?
A: 在 add、remove 操作时会进行加锁,然后复制出来一个新数组,操作的都是新数组,而此时原数组是可以提供查询的。当操作结束之后,会将对象指针指向新数组。
Q: CopyOnWriteArrayList 和 ArrayList 有什么区别?
A: CopyOnWriteArrayList 在读多写少的场景下可以提高效率,而 ArrayList 只是普通数组集合,并不适用于并发场景,而如果对 ArrayList 加锁,则会影响一部分性能。
同样对 CopyOnWriteArrayList 而言,仅能保证最终一致性。因为刚写入的数据,是写到的复制的数组中,此时并不能立即查询到。如果要保证实时性可以尝试使用 Collections.synchronizedList
或者加锁等方式。
Q: CopyOnWriteArrayList 复制是怎么进行复制的?
A: 内部使用的是本地方法 System.arraycopy
进行数组的复制。
结束语
通过阅读 CopyOnWriteArrayList 源码,了解到写时复制是的原理。同时了解到可以使用 System.arraycopy
的方式提高数组复制的效率。
同样 CopyOnWriteArrayList 适合读多写少的场景,满足最终一致性,但是并不能保证数据修改及时查询到。
版权声明: 本文为 InfoQ 作者【程序员小航】的原创文章。
原文链接:【http://xie.infoq.cn/article/545c7d2c7fa97682a91ce002d】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论