看了 CopyOnWriteArrayList 后自己实现了一个 CopyOnWriteHashMap
引言
面试官: 小伙子你有点眼熟啊,是不是去年来这面试过啊。
二胖: 啊,没有啊我这是第一次来这。
面试官: 行,那我们开始今天的面试吧,刚开始我们先来点简单的吧,java
里面的容器你知道哪些啊,跟我说一说吧。
二胖: 好的,java 里面常见容器有ArrayList
(线程非安全)、HashMap
(线程非安全)、HashSet
(线程非安全),ConcurrentHashMap
(线程安全)
面试官: ArrayList
既然线程非安全那有没有线程安全的ArrayList
列?
二胖: 这个。。。 好像问到知识盲点了。
面试官: 那我们今天的面试就先到这了,我待会还有一个会,后续如有通知人事会联系你的。
以上故事纯属虚构如有雷同请以本文为主。
什么是 COW
在 java 里面说到集合容器我们一般首先会想到的是HashMap
、ArrayList
、HasHSet
这几个容器也是平时开发中用的最多的。这几个都是非线程安全的,如果我们有特定业务需要使用线程的安全容器列,
HashMap
可以用ConcurrentHashMap
代替。ArrayList
可以使用Collections.synchronizedList()
方法(list
每个方法都用synchronized
修饰) 或者使用Vector
(现在基本也不用了,每个方法都用synchronized
修饰)或者使用CopyOnWriteArrayList
替代。HasHSet 可以使用
Collections.synchronizedSet
或者使用CopyOnWriteArraySet
来代替。(CopyOnWriteArraySet 为什么不叫 CopyOnWriteHashSet 因为CopyOnWriteArraySet
底层是采用CopyOnWriteArrayList
来实现的)我们可以看到CopyOnWriteArrayList
在线程安全的容器里面多次出现。首先我们来看看什么是CopyOnWrite
?Copy-On-Write
简称COW
,是一种用于程序设计中的优化策略。
CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
为什么要引入 COW
防止 ConcurrentModificationException 异常
在 java 里面我们如果采用不正确的循环姿势去遍历 List 时候,如果一边遍历一边修改抛出java.util.ConcurrentModificationException
错误的。如果对 ArrayList 循环遍历不是很熟悉的可以建议看下这篇文章《ArrayList的删除姿势你都掌握了吗》
上面这个栗子是会发生java.util.ConcurrentModificationException
异常的,如果把ArrayList
改为CopyOnWriteArrayList
是不会发生生异常的。
线程安全的容器
我们再看下面一个栗子一个线程往 List 里面添加数据,一个线程循环 list 读数据。
我们运行上述代码也会发生ConcurrentModificationException
异常,如果把ArrayList
换成了CopyOnWriteArrayList
就一切正常。
CopyOnWriteArrayList 的实现
通过上面两个栗子我们可以发现CopyOnWriteArrayList
是线程安全的,下面我们就来一起看看CopyOnWriteArrayList
是如何实现线程安全的。
从源码中我们可以知道CopyOnWriteArrayList
这和ArrayList
底层实现都是通过一个Object
的数组来实现的,只不过 CopyOnWriteArrayList
的数组是通过volatile
来修饰的,为什么需要volatile
修饰建议可以看看《Java的synchronized 能防止指令重排序吗?》还有新增了ReentrantLock
。
add 方法:
上述源码我们可以发现比较简单,有几个点需要稍微注意下
增加数据的时候是通过
ReentrantLock
加锁操作来(在jdk11
的时候采用了synchronized
来替换ReentrantLock
)保证多线程写的时候只有一个线程进行数组的复制,否则的话内存中会有多份被复制的数据,导致数据错乱。数组是通过
volatile
修饰的,根据volatile
的happens-before
规则,写线程对数组引用的修改是可以立即对读线程是可见的。通过写时复制来保证读写实在两个不同的数据容器中进行操作。
自己实现一个 COW 容器
再 Java 并发包里提供了两个使用CopyOnWrite
机制实现的并发容器,它们是CopyOnWriteArrayList
和CopyOnWriteArraySet
,但是并没有CopyOnWriteHashMap
我们可以按照他的思路自己来实现一个CopyOnWriteHashMap
上述我们实现了一个简单的CopyOnWriteHashMap
,只实现了add、remove、get
方法其他剩余的方法可以自行去实现,涉及到只要数据变化的就要加锁,读无需加锁。
应用场景
CopyOnWrite
并发容器适用于读多写少的并发场景,比如黑白名单、国家城市等基础数据缓存、系统配置等。这些基本都是只要想项目启动的时候初始化一次,变更频率非常的低。如果这种读多写少的场景采用 Vector,Collections
包装的这些方式是不合理的,因为尽管多个读线程从同一个数据容器中读取数据,但是读线程对数据容器的数据并不会发生发生修改,所以并不需要读也加锁。
CopyOnWrite 缺点
CopyOnWriteArrayList 虽然是一个线程安全版的 ArrayList,但其每次修改数据时都会复制一份数据出来,所以 CopyOnWriteArrayList 只适用读多写少或无锁读场景。我们如果在实际业务中使用 CopyOnWriteArrayList,一定是因为这个场景适合而非是为了炫技。
内存占用问题
因为 CopyOnWrite 的写时复制机制每次进行写操作的时候都会有两个数组对象的内存,如果这个数组对象占用的内存较大的话,如果频繁的进行写入就会造成频繁的 Yong GC 和 Full GC。
数据一致性问题
CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。读操作的线程可能不会立即读取到新修改的数据,因为修改操作发生在副本上。但最终修改操作会完成并更新容器所以这是最终一致性。
CopyOnWriteArrayList 和 Collections.synchronizedList()
简单的测试了下 CopyOnWriteArrayList 和 Collections.synchronizedList()的读和写发现:
在高并发的写时 CopyOnWriteArray 比同步 Collections.synchronizedList 慢百倍
在高并发的读性能时 CopyOnWriteArray 比同步 Collections.synchronizedList 快几十倍。
高并发写时,CopyOnWriteArrayList 为何这么慢呢?因为其每次 add 时,都用 Arrays.copyOf 创建新数组,频繁 add 时内存申请释放性能消耗大。
高并发读的时候 CopyOnWriteArray 无锁,Collections.synchronizedList 有锁所以读的效率比较低下。
总结
选择 CopyOnWriteArrayList 的时候一定是读远大于写。如果读写都差不多的话建议选择 Collections.synchronizedList。
结束
由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
感谢您的阅读,十分欢迎并感谢您的关注。
版权声明: 本文为 InfoQ 作者【java金融】的原创文章。
原文链接:【http://xie.infoq.cn/article/d9a735544cc908de4a9a06561】。文章转载请联系作者。
评论