详解CopyOnWrite容器及其源码

jave.util.concurrent包下有这样两个类:CopyOnWriteArrayListCopyOnWriteArraySet
其中利用到了CopyOnWrite机制,本篇就来聊聊CopyOnWrite技术与Java中的CopyOnWrite容器。
主要包扩以下内容:

  • 什么是CopyOnWrite
  • CopyOnWriteArrayList
  • CopyOnWriteArraySet
  • CopyOnWrite适用场景

什么是CopyOnWrite

对于一般的容器,比如ArrayList,在进行并发操作时,如果一个线程读,一个线程写,会抛出java.util.ConcurrentModificationException异常。而CopyOnWrite容器则避免了这种情况。
CopyOnWrite,顾名思义,写时复制,在修改集合中数据的时候,不直接修改当前容器,而是先将当前容器进行拷贝,复制出一个新的容器,然后在新的容器里完成修改,再将原容器的引用指向新的容器。
这样做的好处是,可以不通过加锁,实现对CopyOnWrite容器的并发读写。需要注意的是,CopyOnWrite技术并不保证实时一致性,因为在读写并行时,有可能会读到过期的数据。CopyOnWrite技术保证的是最终一致性。

CopyOnWriteArrayList

CopyOnWriteArrayList的底层是通过数组来实现的,其包含两个属性:

1
2
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;

前者用于在对CopyOnWriteArrayList进行修改时加锁,后者用于保存容器中的元素(允许null元素),对array加了volatile关键字,保证每次修改容器的时候对其他线程都是可见的。
在其各种接口的实现中,用的最多的是如下两个方法:

1
2
3
4
5
6
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}

getArray()方法返回当前数组,而setArray()方法用于在CopyOnWriteArrayList变化时,将array执行修改后的数组内存地址。
CopyOnWriteArrayList提供了三种构造函数:

1
2
3
CopyOnWriteArrayList(); // 创建一个array长度为0的CopyOnWriteArrayList
CopyOnWriteArrayList(Collection<? extends E> c); // 以一个特定容器为参数创建CopyOnWriteArrayList
CopyOnWriteArrayList(E[] toCopyIn); // 以一个数组为参数创建CopyOnWriteArrayList

根据实际的需要创建即可。
CopyOnWriteArrayList提供的读方法与数组的读方法并无什么大的不同,因为CopyOnWriteArrayList本身解决的问题就不是读读并发的问题,所以重一下其写方法。
首先看一下set()方法,set()方法为指定位置设置特定值,如下是其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
Object[] elements = getArray(); // 2
E oldValue = get(elements, index);
if (oldValue != element) { // 3
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len); // 4
newElements[index] = element; // 5
setArray(newElements); // 6
} else {
setArray(elements);
}
return oldValue;
} finally {
lock.unlock(); // 7
}
}

分别看一下以上代码中的关键几步:

  1. 可以看到CopyOnWriteArrayList为了保证在复制原容器时是加了一个可重入锁的,在set完成后释放该锁;
  2. 获取当前的数组;
  3. 判断要设置的位置的旧值与新值是否相同,如果相同则免去容器的拷贝工作;
  4. 将原容器复制一份;
  5. 修改该处的值为新值;
  6. 重新创建CopyOnWriteArrayList容器,将旧容器的内存地址改为新容器所在内存地址;
  7. 完成set,释放锁。
    再看一下add()方法,向容器中添加一个元素,如下是其源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 1
    try {
    Object[] elements = getArray(); // 2
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1); // 3
    newElements[len] = e; // 4
    setArray(newElements); // 5
    return true;
    } finally {
    lock.unlock();
    }
    }

add操作与set操作的大体过程都是相同的,多了两步的是,给新元素新增空间,即3~4步。
上面的add()方法是在容器最后添加一个元素,如果是在指定位置添加一个元素呢,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
Object[] elements = getArray(); // 2
int len = elements.length; // 3
if (index > len || index < 0) // 4
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
Object[] newElements;
int numMoved = len - index; // 5
if (numMoved == 0) // 6
newElements = Arrays.copyOf(elements, len + 1); // 7
else {
newElements = new Object[len + 1]; // 8
System.arraycopy(elements, 0, newElements, 0, index); // 9
System.arraycopy(elements, index, newElements, index + 1, numMoved); // 10
}
newElements[index] = element; // 11
setArray(newElements); // 12
} finally {
lock.unlock(); // 13
}
}

add(int index, E element)相比于add(E e)又多了数组元素移位的过程,即3~10步。移位的时候用到了System.arraycopy()方法,以第9步为例,其意为将elements数组从0开始的index个元素拷贝到newElements数组的从0开始的位置上。System.arraycopy()是一个native方法,用于保证每次add操作对数组移位时的性能不至于太差。
那么从容器中移除一个元素呢,请看其remove()方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
Object[] elements = getArray(); // 2
int len = elements.length; // 3
E oldValue = get(elements, index);
int numMoved = len - index - 1; // 4
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1)); // 5
else {
Object[] newElements = new Object[len - 1]; // 6
System.arraycopy(elements, 0, newElements, 0, index); //7
System.arraycopy(elements, index + 1, newElements, index, numMoved); // 8
setArray(newElements); // 9
}
return oldValue;
} finally {
lock.unlock(); // 10
}
}

remove(index)方法的实现与add(index, element) 方法的实现是类似的,区别在于前者是数组缩小。
除此之外,还提供了remove(Object o)方法用于移除特定值,remove(Object o, Object[] snapshot, int index)方法用于移除特定版本数组下的特定值,且前者的实现是以后者为基础的,故而前者在自己的实现中没有加锁。这里仅拿出第三个remove方法的源码来作分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
Object[] current = getArray(); // 2
int len = current.length;
if (snapshot != current) findIndex: { // 3
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) { // 4
index = i;
break findIndex;
}
}
if (index >= len) // 不存在要删除的元素
return false;
if (current[index] == o)
break findIndex;
index = indexOf(o, current, index, len); // 获取current数组中从index开始到len的值为o的第一个元素的位置
if (index < 0) // 不存在要删除的元素
return false;
}
Object[] newElements = new Object[len - 1]; // 5
System.arraycopy(current, 0, newElements, 0, index); // 6
System.arraycopy(current, index + 1, newElements, index, len - index - 1); // 7
setArray(newElements); // 8
return true;
} finally {
lock.unlock(); // 9
}
}

这个remove方法给定要删除的值和一个数组,以及结束的位置。这个snapshot数组可以认为是某一个版本的array数组,当二者相同时,remove方法和remove(o)就几乎一样了;当二者不同时,即3、4步,则是记录当前数组中与要删除的值相同的那个元素的位置,此时说明snapshot数组已经修改过了,所以相同位置的那个元素已经不同了。
以上是CopyOnWriteArrayList的源码中重要属性和函数的实现剖析。

CopyOnWriteArraySet

了解了CopyOnWriteArrayList的实现之后,CopyOnWriteArraySet的实现就比较简单了,看一眼CopyOnWriteArraySet保存元素的结构就知道为何了:

1
2
3
4
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}

可以看到CopyOnWriteArraySet的实现是基于CopyOnWriteArrayList来做的,CopyOnWriteArraySet提供的各种方法也都是通过CopyOnWriteArrayList来实现,原理基本相同,就不单独详细说明了。

CopyOnWrite适用场景

考虑到每次CopyOnWrite容器进行修改的时候都需要加锁和对容器进行拷贝,写的性能开销较大,所以更适合使用在读操作远远大于写操作的场景里,比如缓存、搜索引擎对某些关键词过滤使用的黑名单等。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主的情况。
因为CopyOnWrite容器只能保证最终一致性,所以不适用于对数据实时性要求较高的场景中,因为一个线程修改了数据,其他线程并不一定能够马上读取到新的数据。

关注我的公众号,获取更多关于面试、技术的文章及福利资源。

详解CopyOnWrite容器及其源码的更多相关文章

  1. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  2. 【Devops】【docker】【CI/CD】关于jenkins构建成功后一步,执行的shell命令详解+jenkins容器运行宿主机shell命令的实现方法

    1.展示这段shell命令 +详解 #================================================================================= ...

  3. 移动端js触摸touch详解(附带案例源码)

    移动端触摸滑动原理详解案例,实现过程通过添加DOM标签的触摸事件监听,并计算触摸距离,通过距离坐标计算触摸角度,最后通过触摸角度去判断往哪个方向触摸的. 触摸的事件列表 触摸的4个事件: touchs ...

  4. 详解web容器 - Jetty与Tomcat孰强孰弱

    Jetty 基本架构 Jetty目前的是一个比较被看好的 Servlet 引擎,它的架构比较简单,也是一个可扩展性和非常灵活的应用服务器.它有一个基本数据模型,这个数据模型就是 Handler(处理器 ...

  5. ThreadLocal类详解:原理、源码、用法

    以下是本文目录: 1.从数据库连接探究 ThreadLocal 2.剖析 ThreadLocal 源码 3. ThreadLocal 应用场景 4. 通过面试题理解 ThreadLocal 1.从数据 ...

  6. (二十三)原型模式详解(clone方法源码的简单剖析)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 原型模式算是JAVA中最简单 ...

  7. AlexNet 网络详解及Tensorflow实现源码

    版权声明:本文为博主原创文章,未经博主允许不得转载. 1. 图片数据处理 2. 卷积神经网络 2.1. 卷积层 2.2. 池化层 2.3. 全链层 3. AlexNet 4. 用Tensorflow搭 ...

  8. 设计模式之 原型模式详解(clone方法源码的简单剖析)

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 原型模式算是JAVA中最简单 ...

  9. 详解重定向(HTTP状态码301/302/303/307/408)附例子

    本文为原创文章,转载请注明出处. 今天打算好好把状态码301.302.303.307.308好好撸一遍,并会测试下一些例子. 状态码的解释 我们都知道重定向与这几种状态码有关,来看下这几种HTTP状态 ...

随机推荐

  1. Redis-缓存穿透、缓存雪崩、缓存击穿、缓存一致性、并发竞争

    缓存流程 在讲这五个问题之前,首先我们回顾下正常的缓存的使用流程 程序在处理请求时,会先从缓存中进行查询,如果缓存中没有对应的key,则会从数据库中查询,如果查询到结果,并将查询结果添加到缓存中去,反 ...

  2. Java 利用Map集合计算一个字符串中每个字符出现的次数

    步骤分析 1.给出一串字符串,字符串中可以包含字母.数字.符号等等. 2.创建一个Map集合,key是字符串中的字符,value是字符的个数. 3.遍历字符串,获取每一个字符. 5.使用获取到的字符, ...

  3. cogs 1440. [NOIP2013]积木大赛 贪心水题

    1440. [NOIP2013]积木大赛 ★★   输入文件:BlockNOIP2013.in   输出文件:BlockNOIP2013.out   简单对比时间限制:1 s   内存限制:128 M ...

  4. 图解kubernetes服务打散算法的实现源码

    在分布式调度中为了保证服务的高可用和容灾需求,通常都会讲服务在多个区域.机架.节点上平均分布,从而避免单点故障引起的服务不可用,在k8s中自然也实现了该算法即SelectorSpread, 本文就来学 ...

  5. C#中TripleDES对应Java中的DESede即大家说的3DES,附C#及Java加解密结果一致的控制台程序例子

    直接上代码了. Java控制台代码: package Test; import java.security.Key; import javax.crypto.Cipher; import javax. ...

  6. spring-cloud-gateway报错

    2019-08-13 09:41:19.216 WARN [-,,,] 10084 --- [ main] ConfigServletWebServerApplicationContext : Exc ...

  7. 04-String

    动手动脑 请运行以下示例代码StringPool.java,查看其输出结果.如何解释这样的输出结果?从中你能总结出什么? s0,s1,s2字符串的内容相同,实际占用的是同一空间,引用的是同一个对象,所 ...

  8. manually Invoking Model Binding / Model Binding /Pro asp.net mvc 5

    限制绑定器 数据源

  9. SpringBoot_Web开发_定制错误数据

    SpringBoot默认的错误处理机制 默认效果: ​ 1).浏览器,返回一个默认的错误页面 2).如果是其他客户端,默认响应一个json数据 原理: ​ 可以参照ErrorMvcAutoConfig ...

  10. GPU与CPU

    GPU与CPU CPU CPU,也就是中央处理器,结构主要包括控制器(指挥各部分工作).运算器(实现数据加工).寄存器.高缓以及数据/控制/状态总线.计算机的性能很大程度上依赖于CPU,CPU的功能包 ...