写时复制集合 —— CopyOnWriteArrayList
前言
JUC 下面还有一个系列的类,都是 CopyOnWriteXXX ,意思是写时复制,这个究竟是怎么回事?那就以 CopyOnWriteArrayList 为切入点,一起了解写时复制是怎么回事?
公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!
介绍
ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
像名字一样,每次进行操作的时候,都会进行一次复制,当然会有很大的性能消耗,但是在某些使用场景下,又会提高性能。具体是怎么操作的,那就一步一步阅读源码,然后再做总结归纳。
基本使用
public class CopyOnWriteArrayListTest {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 添加元素
list.add("liuzhihang");
// 移除元素
list.remove("liuzhihang");
// 查看元素
String value0 = list.get(0);
// 遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
}
}
}
问题疑问
- 为什么要叫写时复制集合?
- CopyOnWriteArrayList 实现原理是什么?
- CopyOnWriteArrayList 和 ArrayList 有什么区别?
- CopyOnWriteArrayList 复制是怎么进行复制的?
源码分析
基本结构

参数介绍
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 数据有变动时使用 */
final transient ReentrantLock lock = new ReentrantLock();
/** 数组 只能通过 getArray/setArray 访问 */
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
// 将数组指向传入的新数组
final void setArray(Object[] a) {
array = a;
}
}
通过参数可以了解到以下内容:
- 基于数组实现;
- 使用了 ReentrantLock 互斥锁。
构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
在初始化 CopyOnWriteArrayList 时,就是创建了一个 Object 的数组。
add
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 方法逻辑很简单:
- 通过加互斥锁(ReentrantLock)从而保证在写的时候只有一个线程可以写。
- 新增元素时,先使用
Arrays.copyOf(elements, len + 1)复制出一个长度 +1 的新数组。 - 添加元素到新数组。
- 然后再将原数组对象指向新数组。
画图如下:

remove
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();
}
}
remove 方法相对多了一些判断:
- 通过加互斥锁(ReentrantLock)从而保证在写的时候只有一个线程可以移除元素。
- 如果移除的是最后一个元素,则直接复制前面的元素到新数组,并指向新数组即可。
- 如果移除的是中间的元素,则需要进行两次复制,然后指向新数组。
画图如下:

get
public E get(int index) {
return get(getArray(), index);
}
get 方法可以看出:
- 获取元素并没有进行加锁。
- 从原数组获取的元素。
所以并发情况下,并不能保证很及时的读取的刚插入或者移除的元素。
数组复制
通过阅读 add 和 remove 相关代码,可以看到在数组复制时使用了 Arrays.copyOf 和 System.arraycopy,这相当于一个优化方面吧。
毕竟数组复制总不能把原数组遍历一遍,挨着赋值到新数组里面吧。
那接下来看一下内部是如何实现的:
public class Arrays {
// 其他方法省略 ...
/**
* original 要复制的数组
* newLength 新数组的长度
*/
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
/**
* original 要复制的数组
* newLength 新数组的长度
*/
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 创建一个新数组,长度是指定的长度
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
// 创建具有指定的组件类型和长度的新数组
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 调用 System.arraycopy 复制数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
}
通过阅读 Arrays.copyOf 相关源码,发现其实 Arrays.copyOf 底层也是调用的 System.arraycopy
public final class System {
// 其他方法省略 ...
/**
* src 源数组
* srcPos 源数组起始位置
* dest 目标数组
* destPos 目标数组的起始位置
* length 要复制的元素数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
}
可以看到 System.arraycopy 是一个 native 方法,这个是 JVM 内部实现的,具体可以阅读相关资料。而使用这种方式要比 for 循环和 clone 要高效很多。
总结
Q&A
Q: 为什么要叫写时复制集合?
A: 因为在 add、remove 操作时会复制出来一个新数组。
Q: CopyOnWriteArrayList 实现原理是什么?
A: 在 add、remove 操作时会进行加锁,然后复制出来一个新数组,操作的都是新数组,而此时原数组是可以提供查询的。当操作结束之后,会将对象指针指向新数组。
Q: CopyOnWriteArrayList 和 ArrayList 有什么区别?
A: CopyOnWriteArrayList 在读多写少的场景下可以提高效率,而 ArrayList 只是普通数组集合,并不适用于并发场景,而如果对 ArrayList 加锁,则会影响一部分性能。
同样对 CopyOnWriteArrayList 而言,仅能保证最终一致性。因为刚写入的数据,是写到的复制的数组中,此时并不能立即查询到。如果要保证实时性可以尝试使用 Collections.synchronizedList 或者加锁等方式。
Q: CopyOnWriteArrayList 复制是怎么进行复制的?
A: 内部使用的是本地方法 System.arraycopy 进行数组的复制。
结束语
通过阅读 CopyOnWriteArrayList 源码,了解到写时复制是的原理。同时了解到可以使用 System.arraycopy 的方式提高数组复制的效率。
同样 CopyOnWriteArrayList 适合读多写少的场景,满足最终一致性,但是并不能保证数据修改及时查询到。
写时复制集合 —— CopyOnWriteArrayList的更多相关文章
- JAVA中写时复制(Copy-On-Write)Map实现
1,什么是写时复制(Copy-On-Write)容器? 写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改.修改 ...
- Java进阶知识点6:并发容器背后的设计理念 - 锁分段、写时复制和弱一致性
一.背景 容器是Java编程中使用频率很高的组件,但Java默认提供的基本容器(ArrayList,HashMap等)均不是线程安全的.当容器和多线程并发编程相遇时,程序员又该何去何从呢? 通常有两种 ...
- php 内核变量 引用计数器写时复制
写时复制,是一个解决内存复用的方法,就是你在php语言层,如$d=$c=$b=$a='value';把$a赋给另一个或多个变量,这时这个变量都只占用一个内存块,当其中一个变量值改变时,才会开辟另一个内 ...
- 线程不安全(Arraylist,HashSet,HashMap)和写时复制
package com.yangyuanyuan.juc1205; import java.util.List; import java.util.Map; import java.util.Set; ...
- phpCOW机制(写时复制)
写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改. COW最早应用在*nix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中,如C ...
- php变量 写时改变 写时复制
写时复制 $var = 1; $var2 = $var; #此时$var2 与 $var 指向同一个zval refcount = 2: $var = 2; # 此时$val 改变 所以 $var 与 ...
- PHP写时复制, 变量复制和对象复制不同!!!
2016年3月18日 15:09:28 星期五 一直以为PHP对象也是写时复制....... 其实: PHP的变量是写时复制, 对象是引用的 写时复制: $a = $b; 如果$b的内容不改变, $a ...
- php 垃圾回收机制----写时复制和引用计数
PHP使用引用计数和写时复制来管理内存.写时复制保证了变量间复制值不浪费内存,引用计数保证了当变量不再需要时,将内存释放给操作系统. 要理解PHP内存管理,首先要理解一个概念----符号表. 符号表的 ...
- fork()和写时复制
写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork( )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...
随机推荐
- 使用 jsDelivr 免费加速 GitHub Pages 博客的静态资源(二)
之前写过一篇 使用 jsDelivr 免费加速 GitHub Pages 博客的静态资源,在那之后,又陆续想到并实施了几点利用 jsDelivr 进一步加速静态资源加载的措施,新起一篇作为记录和分享. ...
- Win10桌面不见了只显示开始菜单该怎么办?
来源:http://www.w10zj.com/Win10xy/Win10xf_4256.html 在Win10系统中,有用户反应桌面不见了,只显示开始菜单的情况,该怎么办呢?出现这样的情况一般由于桌 ...
- C++extern关键字理解
extern是一种"外部声明"的关键字,字面意思就是在此处声明某种变量或函数,在外部定义. 下面的示意图是我的理解. extern关键字的主要作用是扩大变量/函数的作用域,使得其它 ...
- P3660 [USACO17FEB]Why Did the Cow Cross the Road III G
Link 题意: 给定长度为 \(2N\) 的序列,\(1~N\) 各处现过 \(2\) 次,i第一次出现位置记为\(ai\),第二次记为\(bi\),求满足\(ai<aj<bi<b ...
- 2-K8S常用命令
kubectl 命令行管理工具 类型 命令 描述 基础命令 create 通过文件名或标准输入创建资源 expose 为Deployment,Pod创建service run 在集群中运行一个特定的镜 ...
- Rust之路(3)——数据类型 下篇
[未经书面同意,严禁转载] -- 2020-10-14 -- 架构是道,数据是术.道可道,非常道:术不名,不成术!道无常形,术却可循规. 学习与分析数据类型,最基本的方法就是搞清楚其存储原理,变量和对 ...
- MeteoInfoLab脚本示例:subplot
subplot命令可以自动等间距分配多个坐标系(Axes),命令中有三个参数,前两个定义了行数和列数,第三个指定了当前的坐标系(Axes),绘图命令是作用在当前坐标系中的. 脚本程序: def f(t ...
- 题解 CF1428A 【Box is Pull】
通过理解题意,我们发现: 当需要拐弯的时候,兔子需要先走回箱子的位置,再走向拐弯的方向.则拐弯操作的花费为 \(2\) .而直行的操作花费为 \(1\) . 所以, 如果不需要拐弯,也就是 \(x1= ...
- utf-8和utf-8-sig的区别
前言:在写入csv文件中,出现了乱码的问题. 解决:utf-8 改为utf-8-sig 区别如下: 1."utf-8" 是以字节为编码单元,它的字节顺序在所有系统中都是一样的,没有 ...
- Git版本管理器操作步骤
组长建立管理器: 第一步: 管理器网址:https://gitee.com/ 第二步:注册个人信息 第三步(添加项目) 第四步(兴建文件)注意:必须双层文件夹 第五步:打开VS开发工具 第六步:去把刚 ...