剖析 CopyOnWriteArrayList
原文链接:https://www.changxuan.top/?p=1252
CopyOnWriteArrayList 是 JUC 中唯一一个支持并发的 List。
CopyOnWriteArrayList 的修改操作都是在底层的一个复制的数组上进行,即写时复制策略,从而实现了线程安全。其实原理和数据库的读写分离十分相似。
基本构成
底层使用数组 private transient volatile Object[] array; 来存储元素,使用 ReentrantLock 独占锁保证相关操作的安全性。
构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 将集合 c 内的元素复制到 list 中
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
// 创建一个内部元素是 toCopyIn 副本的 list
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
添加元素
CopyOnWriteArrayList 中与添加元素相关的方法有以下几种:
- add(E e)
- add(int index, E element)
- addAll(Collection<? extends E> c)
- addAll(int index, Collection<? extends E> c)
- addIfAbsent(E e)
- addAllAbsent(Collection<? extends E> c)
鉴于原理基本相似,下面只分析 add(E e) 和 addIfAbsent(E e) 方法做为例子。
add(E e)
源码
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();
}
}
进入 add 方法的线程首先会去尝试获取独占锁,成功获取的线程会继续执行后续添加元素逻辑,而未获取独占锁的线程在没有异常的情况下则会阻塞挂起。等待独占锁被释放后,再次尝试获取。(ps. 在 CopyOnWriteArrayList 中使用的是 ReentrantLock 的非公平锁模式)
这样就能保证,同一时间只有一个线程进行添加元素。
addIfAbsent(E e)
源码
public boolean addIfAbsent(E e) {
// 获取当前数组
Object[] snapshot = getArray();
// 调用 indexOf 判断元素是否已存在 (遍历)
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;
}
// 与 add 方法的逻辑相同
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
如果当前数组中不存在元素 e,addIfAbsent 则会将 e 添加至数组中返回 true;如果当前数组中存在待添加元素,则会返回 false。
在 addIfAbsent 方法中为了提高性能,设计者把判断“当前数组是否存在待添加元素”和“添加元素”的操作分开了。由于前一个操作不必要获取独占锁,在遇到每次待添加的元素都已经存在于数组的情况时可以高效的返回 false 。
因为上面提到的两步操作是非原子性的,所以再第二步操作中还需要再次进行确认之前用来判断不存在元素 e 的数组是否被“掉包”了。如果被“掉包”,那么也不要“嫌弃”。就需要再判断一下“掉包”后的数组还能不能接着用。如果不能用直接返回 false,如果发现能用就继续向下执行,成功后返回 true 。
这种设计思路,在自己的业务系统中还是比较值的借鉴的。当然上述场景下“坏”的设计,就是会先尝试获取独占锁,在获取独占锁后再进行“判断元素是否存在和决定是否添加元素的操作”。这样则会导致大大增加线程阻塞挂起几率。相信大多数同学还是能写出漂亮的代码的,不至于犯这种小错误。
获取元素
获取元素一共涉及到三个方法,源码如下:
public E get(int index) {
return get(getArray(), index);
}
// 步骤一
final Object[] getArray() {
return array;
}
// 步骤二
private E get(Object[] a, int index) {
return (E) a[index];
}
我们看到获取元素的操作,全程没有加锁。并且获取元素是由两步操作组合而成的,一获取当前数组,二从当前数据中取出所指定的下标位置的元素。一旦在这两步操作之间,有其它线程更改了 index 下标位置的元素。此时,获取元素的线程所使用的数组则是被废弃掉的,它接收到的值也不是最新的值。这是写时复制策略产生的弱一致性问题。
修改元素
可以使用 set(int index, E element) 方法修改指定位置的元素。
源码
public E set(int index, E element) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前数组
Object[] elements = getArray();
// 获取要修改位置的元素
E oldValue = get(elements, index);
// 新值与老值是否一样
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
可以看到,在代码中并没有显示的判断 index 是否合法,如果不合法则会抛出 IndexOutOfBoundsException 异常。
主要逻辑也是先尝试获取独占锁,符合条件则进行修改。需要注意的一点是,如果指定索引处的元素值与新值相等,也会调用 setArray(Object[] a) 一次方法,这主要是为了保证 volatile 语义。(线程在写入 volatile 变量时,不会把值缓存在寄存器或者其它地方,而是会把值刷回到主内存,确保内存可见性)
删除元素
删除元素的方法包括:
- E remove(int index)
- boolean remove(Object o)
- boolean removeAll(Collection<?> c)
我们来看下 remove(int index) 的实现,
源码
public E remove(int index) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前数据
Object[] elements = getArray();
int len = elements.length;
// 获取要被删除的元素
E oldValue = get(elements, index);
// 计算要移动的位置
int numMoved = len - index - 1;
if (numMoved == 0)
// 删除最后一个元素
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
// 复制被删除元素之前的所有元素到新数组
System.arraycopy(elements, 0, newElements, 0, index);
// 复制被删除元素之后的所有元素到新数组
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 设置新数组
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
其实代码逻辑很清楚,获取锁后根据情况复制老数组中的未删除数据到新数组即可。
迭代器
不知道大家有没有在遍历 ArrayList 变量的过程中想没想过删除其中的某个元素?反正我曾经这么写过,然后就出现了问题 ... 后来使用了 ArrayList 的迭代器之后就没有错误了。
在 CopyOnWriteArrayList 中也有迭代器,但是也存在着弱一致性问题
源码
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array 数组的快照版本 */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
// 构造函数
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 是否结束
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
// 获取元素
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
... ...
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
... ...
}
可以看到,CopyOnWriteArrayList 的迭代器并不支持 remove 操作。在调用 iterator() 方法时获取了一份当前数组的快照,如果在遍历期间并没有其它线程对数据做更改操作就不会出现一致性的问题。一旦有其它线程对数据更改后,将 CopyOnWriteArrayList 中的数组更改为了新数组,此时迭代器所持有的数据就相当于快照了,同时也出现了弱一致性问题。
拓展延申
还记得刚刚提到的 addIfAbsent 方法吗?看到它你有没有联想到什么东西呢?集合 set?
对的,通过 addIfAbsent 方法也能实现集合的功能,CopyOnWriteArraySet 的底层就是使用 CopyOnWriteArrayList 实现的。(PS. HasSet 的底层依赖 HashMap 。)
剖析 CopyOnWriteArrayList的更多相关文章
- 并发编程之 CopyOnWriteArrayList 源码剖析
前言 ArrayList 是一个不安全的容器,在多线程调用 add 方法的时候会出现 ArrayIndexOutOfBoundsException 异常,而 Vector 虽然安全,但由于其 add ...
- MapReduce剖析笔记之二:Job提交的过程
上一节以WordCount分析了MapReduce的基本执行流程,但并没有从框架上进行分析,这一部分工作在后续慢慢补充.这一节,先剖析一下作业提交过程. 在分析之前,我们先进行一下粗略的思考,如果要我 ...
- ArrayList源码剖析
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- 转:【Java集合源码剖析】ArrayList源码剖析
转载请注明出处:http://blog.csdn.net/ns_code/article/details/35568011 本篇博文参加了CSDN博文大赛,如果您觉得这篇博文不错,希望您能帮我投一 ...
- CopyOnWriteArrayList你都不知道,怎么拿offer?
前言 只有光头才能变强 前一阵子写过一篇COW(Copy On Write)文章,结果阅读量很低啊...COW奶牛!Copy On Write机制了解一下 可能大家对这个技术比较陌生吧,但这项技术是挺 ...
- Spark源码剖析 - SparkContext的初始化(三)_创建并初始化Spark UI
3. 创建并初始化Spark UI 任何系统都需要提供监控功能,用浏览器能访问具有样式及布局并提供丰富监控数据的页面无疑是一种简单.高效的方式.SparkUI就是这样的服务. 在大型分布式系统中,采用 ...
- 并发编程之 ConcurrentLinkedQueue 源码剖析
前言 今天我们继续分析 java 并发包的源码,今天的主角是谁呢?ConcurrentLinkedQueue,上次我们分析了并发下 ArrayList 的替代 CopyOnWriteArrayList ...
- 【Java集合源代码剖析】ArrayList源代码剖析
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/mmc_maodun/article/details/35568011 转载请注明出处:http:// ...
- 性能测试工具 nGrinder 项目剖析及二次开发
转:https://testerhome.com/topics/4225 0.背景 组内需要一款轻量级的性能测试工具,之前考虑过LR(太笨重,单实例,当然它的地位是不容置疑的),阿里云的PTS(htt ...
随机推荐
- ViperX 300 Robot Arm 机械臂 “5自由度和360°全方位旋转”
- 第六章节 BJROBOT 动态导航壁障
导航前说明:一定要确保你小车在构建好地图的基础上进行! 1.把小车平放在你想要构建地图区域的地板上,打开资料里的虚拟机,打开一个终端, ssh 过去主控端启动 roslaunch znjrobot b ...
- 聊聊并发,进程通信方式,go协程简单应用场景
开篇提问 知道并发,并行,线程,协程概念吗?或者知道大概含义吗? 有线程为什么还要有协程?区别是什么? 『进程』通信方式知道几种?有没有超过3种? golang『协程』通信方式推荐? 使用并发的目的是 ...
- Supervisord进程管家
Supervisord进程管家 Supervisord是一个守护进程的工具,当进程意外终止或服务器掉电起来后,希望进程能够自动运行,supervisord可以很好的为我们做这件事情.同时supervi ...
- Haproxy-1.8.20 根据路径(URI)转发到后端不同集群
HAProxy根据不同的URI 转发到后端的服务器组 1 ) 实验内容说明: 1.1 ) 根据不同的URI 转发到后端的服务器组. /a /b 和其他 默认使用其他. 1.2 ) 使用IP介绍: ha ...
- AttGAN: Facial Attribute Editing by Only Changing What You Want 论文阅读笔记和AttGan的pytorch代码实现
1.总体框架 上面的过程用详细描述即是 Test阶段: Train阶段: 由于我们无法得知编辑后的image,所以显而易见人脸属性编辑是一个无监督问题,而对于我们的xa需要获得关于b的属性,故利用at ...
- Hbase RIT故障修复
业务场景: RocketMQ+Storm+Hbase 组件版本: RocketMQ:3.4.6 Storm:1.2.1 Hbase:1.2.1 1. 问题描述 4月15号早上发现业务系统前一天数据量明 ...
- 【Linux】使用 iperf 测试 Linux 服务器带宽
iperf 简介 iperf 是一个用于测试网络带宽的命令行工具,可以测试服务器的网络吞吐量.目前发现两个很实用的功能: 测试服务器网络吞吐量:如果我们需要知道某台服务器的「最大」网络带宽,那么最好在 ...
- 【EXP/IMP】问题总结
为了使测试与生产数据保持一致,只需要导出数据的时候,可以将测试库的表truncate,保留其它如索引,trigger,constraints,grants等不用再重新导. exp时候rows=y,其它 ...
- 写给 Linux 初学者的一封信
大家好,我是肖邦. 这篇文章是写给 Linux 初学者的,我会分享一些作为初学者应该知道的一些东西,这些内容都是本人从事 Linux 开发工作多年的心得体会,相信会对初学者有所帮助.如果你是 Linu ...