JUC源码分析-集合篇(四)CopyOnWriteArrayList
JUC源码分析-集合篇(四)CopyOnWriteArrayList
Copy-On-Write 简称 COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容 Copy 出去形成一个新的内容然后再改,这是一种延时懒惰策略。从 JDK1.5 开始 Java 并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器:
CopyOnWriteArrayListArrayList 线程安全的实现CopyOnWriteArraySetSet 线程安全的实现
1. CopyOnWrite 容器
1.1 什么是 CopyOnWrite 容器
CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
1.2 CopyOnWrite 应用场景
CopyOnWrite 并发容器用于读多写少的并发场景。使用 CopyOnWriteMap 需要注意两件事情:
减少扩容开销。根据实际需要,初始化 CopyOnWriteMap 的大小,避免写时 CopyOnWriteMap 扩容的开销。
使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
1.3 CopyOnWrite 缺点
CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
- 内存占用问题。因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说 200M 左右,那么再写入 100M 数据进去,内存就会占用 300M,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。之前我们系统中使用了一个服务由于每晚使用 CopyOnWrite 机制更新大对象,造成了每晚 15 秒的 Full GC,应用响应时间也随之变长。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是 10 进制的数字,可以考虑把它压缩成 36 进制或 64 进制。或者不使用 CopyOnWrite 容器,而使用其他的并发容器,如 ConcurrentHashMap。
- 数据一致性问题。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();
}
}
参考:
每天用心记录一点点。内容也许不重要,但习惯很重要!
JUC源码分析-集合篇(四)CopyOnWriteArrayList的更多相关文章
- JUC源码分析-集合篇:并发类容器介绍
JUC源码分析-集合篇:并发类容器介绍 同步类容器是 线程安全 的,如 Vector.HashTable 等容器的同步功能都是由 Collections.synchronizedMap 等工厂方法去创 ...
- JUC源码分析-集合篇(九)SynchronousQueue
JUC源码分析-集合篇(九)SynchronousQueue SynchronousQueue 是一个同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然.SynchronousQu ...
- JUC源码分析-集合篇(三)ConcurrentLinkedQueue
JUC源码分析-集合篇(三)ConcurrentLinkedQueue 在并发编程中,有时候需要使用线程安全的队列.如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法. ...
- JUC源码分析-集合篇(十)LinkedTransferQueue
JUC源码分析-集合篇(十)LinkedTransferQueue LinkedTransferQueue(LTQ) 相比 BlockingQueue 更进一步,生产者会一直阻塞直到所添加到队列的元素 ...
- JUC源码分析-集合篇(八)DelayQueue
JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...
- JUC源码分析-集合篇(七)PriorityBlockingQueue
JUC源码分析-集合篇(七)PriorityBlockingQueue PriorityBlockingQueue 是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现. P ...
- JUC源码分析-集合篇(六)LinkedBlockingQueue
JUC源码分析-集合篇(六)LinkedBlockingQueue 1. 数据结构 LinkedBlockingQueue 和 ConcurrentLinkedQueue 一样都是由 head 节点和 ...
- JUC源码分析-集合篇(一)ConcurrentHashMap
JUC源码分析-集合篇(一)ConcurrentHashMap 1. 概述 <HashMap 源码详细分析(JDK1.8)>:https://segmentfault.com/a/1190 ...
- JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor
JUC源码分析-线程池篇(三)ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor.它主要用来在 ...
随机推荐
- upc组队赛3 Chaarshanbegaan at Cafebazaar
Chaarshanbegaan at Cafebazaar 题目链接 http://icpc.upc.edu.cn/problem.php?cid=1618&pid=1 题目描述 Chaars ...
- 3.Jmeter 快速入门教程(三-1) --添加响应断言(即loadrunner中所指的检查点)
上一节课,我们创建了一个测试场景,并进行了少量vuser的负载测试. 有时候我们执行了测试,但是发现并不是所有事务都执行成功了. 那是因为我们只是发起了测试,但并没有对每次请求测试的返回作校验. 所以 ...
- java 重新学习 (四)
一.内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类中的成员之间可以相互访问.但外部类不能访问内部类的实现细节,例如内部类的成员变量.匿名内部类适合用于创建仅需要一次使用 ...
- 马士兵对话京东T6阿里P7(薪水):月薪5万,他为何要离职?
马士兵大佬你知道吗? 你竟然不知道?你怎么可能不知道!你不知道是不可能的! 记得自己的第一行Java代码,你的Hello World是跟着谁学的吗?我的就是马士兵老师! 马士兵是唯一一个在当时讲课是让 ...
- 19-python基础-进制之间的转换
二进制-八进制-十进制-十六进制相互转换 1.十进制转为其他进制 # (1)十进制转二进制 a = 8 bin(a) --->>'0b1000' # (2)十进制转八进制 oct(a) - ...
- 怎么在vue-cli中利用 :class去做一个底层背景或者文字的点击切换
// html <div class="pusherLists" :class="{hidden: isHidden}"> <span @cl ...
- git拉取远程所有分支
第一步: git branch -r | grep -v '->' | while read remote; do git branch --track "${remote#origi ...
- Leetcode 算法题
lEETCODE 算法题 0013 罗马数字转整数
- GitHub-Hexo-Blog 集成Gitalk评论插件
在本文)末尾可查看先查看效果: 1. 新建New OAuth App 在github中,Settings / Develpoer settings OAuth Apps / New OAuth App ...
- Samba服务的安装
Samba的安装 1.准备环境 Centos7 [root@localhost ~]# systemctl stop firewalld [root@localhost ~]# setenforce ...