JUC源码分析-集合篇(四)CopyOnWriteArrayList

Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容 Copy 出去形成一个新的内容然后再改,这是一种延时懒惰策略。从 JDK1.5 开始 Java 并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器:

  • CopyOnWriteArrayList ArrayList 线程安全的实现
  • CopyOnWriteArraySetSet 线程安全的实现

1. CopyOnWrite 容器

1.1 什么是 CopyOnWrite 容器

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

1.2 CopyOnWrite 应用场景

CopyOnWrite 并发容器用于读多写少的并发场景。使用 CopyOnWriteMap 需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化 CopyOnWriteMap 的大小,避免写时 CopyOnWriteMap 扩容的开销。

  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。

1.3 CopyOnWrite 缺点

CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

  1. 内存占用问题。因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说 200M 左右,那么再写入 100M 数据进去,内存就会占用 300M,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。之前我们系统中使用了一个服务由于每晚使用 CopyOnWrite 机制更新大对象,造成了每晚 15 秒的 Full GC,应用响应时间也随之变长。

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是 10 进制的数字,可以考虑把它压缩成 36 进制或 64 进制。或者不使用 CopyOnWrite 容器,而使用其他的并发容器,如 ConcurrentHashMap。

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

2. CopyOnWriteArrayList 实现原理

List<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
list.get(0);

在使用 CopyOnWriteArrayList 之前,我们先阅读其源码了解下它是如何实现的。以下代码是向 ArrayList 里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会 Copy 出 N 个副本出来。

public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

读的时候不需要加锁,如果读的时候有多个线程正在向 ArrayList 添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的 ArrayList。

public E get(int index) {
return get(getArray(), index);
}

实现很简单,只要了解了 CopyOnWrite 机制,我们可以实现各种 CopyOnWrite 容器,并且在不同的应用场景中使用。

3. CopyOnWriteArraySet 实现原理

CopyOnWriteArraySet 底层全部使用 CopyOnWriteArrayList 实现。

public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }

// 增删改查都是调用 CopyOnWriteArrayList 的方法
public boolean add(E e) { return al.addIfAbsent(e); }
public boolean remove(Object o) { return al.remove(o); }
public boolean contains(Object o) { return al.contains(o); }

以 addIfAbsent 为例

public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
// indexOf 查找指定元素e在snapshot数组中的索引位置
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
} private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

参考:

  1. 聊聊并发-Java中的Copy-On-Write容器

每天用心记录一点点。内容也许不重要,但习惯很重要!

JUC源码分析-集合篇(四)CopyOnWriteArrayList的更多相关文章

  1. JUC源码分析-集合篇:并发类容器介绍

    JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...

  2. JUC源码分析-集合篇(九)SynchronousQueue

    JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...

  3. JUC源码分析-集合篇(三)ConcurrentLinkedQueue

    JUC源码分析-集合篇(三)ConcurrentLinkedQueue 在并发编程中,有时候需要使用线程安全的队列.如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法. ...

  4. JUC源码分析-集合篇(十)LinkedTransferQueue

    JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...

  5. JUC源码分析-集合篇(八)DelayQueue

    JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...

  6. JUC源码分析-集合篇(七)PriorityBlockingQueue

    JUC源码分析-集合篇(七)PriorityBlockingQueue PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现. P ...

  7. JUC源码分析-集合篇(六)LinkedBlockingQueue

    JUC源码分析-集合篇(六)LinkedBlockingQueue 1. 数据结构 LinkedBlockingQueue 和 ConcurrentLinkedQueue 一样都是由 head 节点和 ...

  8. JUC源码分析-集合篇(一)ConcurrentHashMap

    JUC源码分析-集合篇(一)ConcurrentHashMap 1. 概述 <HashMap 源码详细分析(JDK1.8)>:https://segmentfault.com/a/1190 ...

  9. JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor

    JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor.它主要用来在 ...

随机推荐

  1. nginx logformat说明

    记录一下nginx logformat的相关说明 log_format格式变量:$remote_addr  #记录访问网站的客户端地址$remote_user  #远程客户端用户名$time_loca ...

  2. PHP的安装配置

    一.安装 PHP的安装可以很简单的使用yum命令进行安装. #添加php7.0源(这是centos7的命令,centos6.5的命令不同,不要照搬)rpm -Uvh https://dl.fedora ...

  3. Head First PHP &MySQL学习笔记

      最近一段时间在学习PHP,买了<Head First PHP&MySQL>中文版这本书,之前买过<Head First设计模式>,感觉这系列的书籍总体来说很不错. ...

  4. 查看hive版本号

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/sheismylife/article/details/33378243 hive没有提供hive - ...

  5. 基于Linux平台病毒Wirenet.c解析

    在分析Wirenet.c时,感觉自己学到了非常多非常赞的思想,希望跟大家一同交流. 转载请注明出处:http://blog.csdn.net/u010484477谢谢^_^ watermark/2/t ...

  6. 安装 sysbench的 报错 /usr/bin/ld: cannot find -lmysqlclient_r 解决办法

    首先你需要找到这个库的位置 一般找的话需要将lib 给加上(注意:我这里是 -lmysqlclient_r 的报错,于是我找就找 libmysqlclient_r ) find / -name lib ...

  7. 使用Flask-Mail发送邮件

    简介 在WEB开发时,我们常常会使用到发送邮件的功能,注册时或者更换密码时,需要验证邮箱,在flask的扩展中有Flask-mai来帮助完成这一功能 配置 flask-mail发送邮件需要你提供你的邮 ...

  8. 【LeetCode】Math

    [263] Ugly Number [Easy] 一个数的质因子只有2,3,5就叫丑数,写个函数判断丑数. //Author: Wanying //注意 0 和 1 的corner case, 你居然 ...

  9. nodejs模块——fs模块 使用fs.write读文件

    fs.write() fs.read(fd,buffer,offset,length[,position],callback(err,bytesWritten,buffer))接收6个参数. 参数说明 ...

  10. python使用xlrd读取excel数据

    一.安装xlrd 库的安装我这里就不说了.. 二.读取 excel 前提条件:excel文件名称为 excel_data.xlsx 1.打开excelw 文件 workbook = xlrd.open ...