前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CopyOnWrite你都不知道还怎么拿Offer

CopyOnWrite你都不知道还怎么拿Offer

作者头像
业余草
发布2019-04-09 11:08:38
3830
发布2019-04-09 11:08:38
举报
文章被收录于专栏:业余草业余草

CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。

Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容 Copy 出去形成一个新的内容然后再改,这是一种延时懒惰策略。

Java 并发包提供了很多线程安全的集合,有了他们的存在,使得我们在多线程开发下,大大简化了多线程开发的难度,但是如果不知道其中的原理,可能会引发意想不到的问题,所以知道其中的原理还是很有必要的。

CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

下面我们看看它的 add 方法的源码:

640?wx_fmt=png
640?wx_fmt=png

CopyOnWrite 的名字就是这样来的。在写的时候,先 copy 一个,操作新的对象。然后在覆盖旧的对象,保证 volatile 语义。

看完这个源码后,我们来看几个常见的面试题。

CopyOnWriteArrayList 有什么优点?

读写分离,适合写少读多的场景。使用了独占锁,支持多线程下的并发写。

CopyOnWriteArrayList 是如何保证写时线程安全的?

因为用了 ReentrantLock 独占锁,保证同时只有一个线程对集合进行修改操作。

CopyOnWrite 怎么理解?

写时复制。就是在写的时候,先 copy 一个,操作新的对象。然后在覆盖旧的对象,保证 volatile 语义。新数组的长度等于旧数组的长度 + 1。

从 add 方法的源码中你可以看出  CopyOnWriteArrayList 的缺点是什么?

占用内存,写时 copy 效率低。因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说 200M 左右,那么再写入 100M 数据进去,内存就会占用 300M,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。

get 的源码分析。

640?wx_fmt=png
640?wx_fmt=png

get 方法很简单。但是会出现一个很致命的问题,那就是一致性问题。

当我们获得了 array 后,由于整个 get 方法没有独占锁,所以另外一个线程还可以继续执行修改的操作,比如执行了 remove 的操作,remove 和 add 一样,也会申请独占锁,并且复制出新的数组,删除元素后,替换掉旧的数组。而这一切 get 方法是不知道的,它不知道 array 数组已经发生了天翻地覆的变化。就像微信一样,虽然对方已经把你给删了,但是你不知道。这就是一个一致性问题。

CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器。

set 方法解读。

640?wx_fmt=png
640?wx_fmt=png

上面的代码,我写的都有注释,相信大家都能看明白。

set 的时候,同样的会获得一个独占锁,来保证写的线程安全。修改操作,实际上操作的是 array 的一个副本,最后才把 array 给替换掉。所以,修改和 add 很相似。set、add、remove 是互斥的。

remove 方法解读。

640?wx_fmt=png
640?wx_fmt=png

研究 java 自带的一些数据结构,你会发现设计的都很巧妙。大师就是大师啊。

迭代器的主要源码如下:

640?wx_fmt=png
640?wx_fmt=png

调用 iterator 方法获取迭代器,内部会调用 COWIterator 的构造方法,此构造方法有两个参数,第一个参数就是 array 数组,第二个参数是下标,就是 0。随后构造方法中会把 array 数组赋值给snapshot变量。snapshot 是“快照”的意思,如果 Java 基础尚可的话,应该知道数组是引用类型,传递的是指针,如果有其他地方修改了数组,这里应该马上就可以反应出来,那为什么又会是 snapshot这样的命名呢?没错,如果其他线程没有对 CopyOnWriteArrayList 进行增删改的操作,那么 snapshot 就是本身的 array,但是如果其他线程对 CopyOnWriteArrayList 进行了增删改的操作,旧的数组会被新的数组给替换掉,但是 snapshot 还是原来旧的数组的引用。也就是说 当我们使用迭代器便利 CopyOnWriteArrayList 的时候,不能保证拿到的数据是最新的,这也是一致性问题。

CopyOnWriteArrayList 的使用场景

通过源码分析,我们看出它的优缺点比较明显,所以使用场景也就比较明显。就是合适读多写少的场景。

CopyOnWriteArrayList 的缺点

  1. 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
  2. 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
  3. 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWriteArrayList 的设计思想

  1. 读写分离,读和写分开
  2. 最终一致性
  3. 使用另外开辟空间的思路,来解决并发冲突
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年03月19日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档