引言

写时复制的含义是当容器发生修改操作时,如add() 等,就会将原来的容器整体复制一份,这个过程是加锁的。而如果只是读取资源,例如 get() ,就不会受到任何同步要求的限制。

写时复制的理念是,如果多个读取线程请求相同的数据,它们会共享相同的数据,而不需要考虑并发修改的问题不得不在线程内部生成一份数据副本;当容器发生修改操作时,系统这时才会真正复制一个副本给其他请求者,也就是说,写时复制的理念最主要是解决访问者需要考虑并发修改的问题,有了这种机制,就可以避免生成线程副本或加锁访问,只要容器没有被修改,就不会产生任何数据副本。在很大程度上提高了读的性能。

但缺点显而易见,如果容器依然有大量的修改操作,或读写比不高的话,使用写时复制容器只会降低程序性能。

一、CopyOnWriteArrayList

1.1 写入效率

public class T04_CopyOnWriteList {public static void runAndComputeTime(Thread[] ths) {long s1 = System.currentTimeMillis();Arrays.stream(ths).forEach(t -> t.start());Arrays.stream(ths).forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long s2 = System.currentTimeMillis();System.out.println(s2 - s1 + " ms");}public static void main(String[] args) {
//        List<String> list = new CopyOnWriteArrayList<>(); // output:7565 ms
//        List<String> list = new Vector<>(); // output:53 msList<String> list = Collections.synchronizedList(new ArrayList<>()); // output:52 msThread[] ths = new Thread[100];for (int i = 0; i < ths.length; i++) {Runnable task = () -> {for (int j = 0; j < 1000; j++) {list.add("a" + j);}};ths[i] = new Thread(task);}runAndComputeTime(ths);System.out.println(list.size());}
}

上述代码模拟了100个线程,每个线程向 list 中加入1000个字符串的操作。

runAndComputeTime() 方法先是启动线程,然后通过 join 方法,依次将所有线程合并到主线程中,这么做的目的主要是让全部线程的执行时间累加,从而得出一个总时间。

最后输出消耗时间和 list 存储数量,消耗时间不必多说, list 存储数量主要是看并发场景下线程安全性,不能出现“丢失数据”的情况,想一想 如果用 ArrayList,最终 size() 方法输出多少?

从输出结果来看,两个同步容器执行效率相当,都是 50 ~ 80 ms 左右,而CopyOnWriteArrayList 执行效率最长,达到了惊人的 7500 ms。

所以,如果不是确定写入操作极少,就一定不要使用 CopyOnWriteArrayList!

1.2 读取效率

public class T05_CopyOnWriteList {private static List<String> list = new CopyOnWriteArrayList<>(); // output:66ms
//    private static List<String> list = new Vector<>();// output:956ms
//    private static List<String> list = Collections.synchronizedList(new ArrayList<>());// output:873msstatic {for (int i = 0; i < 100000; i++) {list.add(String.valueOf(i));}}public static void main(String[] args) {Thread[] ths = new Thread[100];for (int i = 0; i < ths.length; i++) {Runnable task = () -> {for (int j = 0; j < list.size(); j++) {list.get(j);}};ths[i] = new Thread(task);}System.out.println("开始...");long t1 = System.currentTimeMillis();Arrays.stream(ths).forEach(t -> t.start());Arrays.stream(ths).forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long t2 = System.currentTimeMillis();System.out.println(t2 - t1 + "ms");}
}

上述代码中,先通过 static 块初始化了一个 list,然后通过 100个线程并发读取 list 中的数据,最后通过 join 累加所有过程的执行时间,并输出消耗时间。

从执行结果来看,同步容器的执行效率在 800~900ms 左右,而 CopyOnWriteArrayList 可以达到惊人的 百毫秒以内,效率提升了十倍以上。

所以,如果确定一批数据的写入操作极少,而读取操作非常频繁的话,可以考虑使用 CopyOnWriteArrayList 容器,线程安全+读性能可观。

二、CopyOnWriteArrayList 源码分析

写时复制容器的写入操作包括 add 、set 等,实现逻辑几乎完全一致,以 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();}}

容器内部维护了一个 ReentrantLock 作为锁的实现,在执行 add 操作时,先进行锁定。

然后取得底层数组,并拷贝一个长度 +1 的新数组,并将新元素放入最后。

将容器指针指向新的数组后,unlock 解锁。

由此可见,写入慢的原因就不言自明了,加锁、整个数组拷贝,这两个逻辑就是写入慢的真正的元凶。

我们再来看下读取的逻辑,以 get() 为例:

public E get(int index) {return get(getArray(), index);
}@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {return (E) a[index];
}

整个获取元素的过程未加任何锁,原因就是容器已经在修改的时候保证了同步逻辑,极大的提升了读取的效率。

总结

写时复制的观念就是在修改时复制容器的副本,从而避免在读取时需要考虑额外的并发修改问题。

写时复制容器的应用场景是写入操作极少,读取操作非常多的情况,切不可在包含大量写入操作的场景下使用 CopyOnWrite 。

Java 的写时复制容器实现是 CopyOnWriteArrayList,其底层就是一个定长数组,当容器发生修改时,会使用容器内的 ReentrantLock 上锁,并拷贝整个数组完成操作。

当发生读取时,可以像单线程那样不需要加任何同步机制,可以让多线程并发读取的效率达到最大。

Java 写时复制容器 —— CopyOnWriteArrayList相关推荐

  1. Java 写时拷贝容器CopyOnWriteArrayList的测试

    测试代码: package copyOnWriteArrayListTest;import java.util.ArrayList; import java.util.List; import jav ...

  2. Java写时复制CopyOnWriteArrayList

    Java写时复制CopyOnWriteArrayList Copy-On-Write是一种程序设计的优化方法,多线程在不修改对象时可以共享一个对象地址空间,如果某一个线程要求修改对象时,需要首先将原来 ...

  3. Java源码解读--CopyOnWriteList写时复制集合容器

    加元素时复制,适用于写少读多的场景. 读的时候不加锁,写的时候加锁.Vector的实现是不论读写都加锁. 写的时候复制出一个新的数组,将新添加的元素添加进新的数组,然后将引用指向新的数组地址,因此写的 ...

  4. 写时复制(Copy-On-Write)思想在Java中的应用

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:https://blog.csdn.net/fuzhongmin05/article/details/117076906 ...

  5. 实战并发编程 - 05等效不可变对象CopyOnWriteArrayList适用场景剖析_写时复制COW

    文章目录 Pre 等效不可变对象 实例变量array就是一个等效不可变对象? 写时复制机制 写时复制的目的 优点 缺点 CopyOnWriteArrayList的使用场景 小结 Pre 本篇文章我们分 ...

  6. QTL 容器 与 STL(1)- 写时复制

    QTL 与 STL 最大的区别之一 隐式共享,引用计数,写时复制 [注]STL 的 string 也具有 写时复制 技术 写时复制 (Copy-On-Write)技术,就是编程界"懒惰行为& ...

  7. 写时复制,写时拷贝,写时分裂,Copy on write

    2019独角兽企业重金招聘Python工程师标准>>> 写时复制,写时拷贝,写时分裂 (Copy-on-write,简称COW)是计算机资源管理方面的一种优化技术,有着广泛的应用,比 ...

  8. Linux-Copy On Write写时复制机制初探

    文章目录 生猛干货 COW概述 *Unix fork 函数族exec( ) 为什么有了COW? COW 原理 COW的优缺点 小结 搞定Linux核心技术 生猛干货 从系统安装到程序员必备的Linux ...

  9. 写时复制(Copy On Write)

    前言 在读<Redis设计与实现>关于哈希表扩容的时候,发现这么一段话: 执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操 ...

最新文章

  1. python ---------函数
  2. cad图标注释大全_CAD源泉插件快捷键使用教程(全集)
  3. React开发(228):ant design table根据宽度自动忽略
  4. 如何在苹果官网下载旧版本的Xcode 方法
  5. [设计模式] ------ 模板模式
  6. ironpython console怎么用_如何在表單中插入ironpython控制台?
  7. 我必须要吐槽,你们的数据管理都是错的,这才是规划和产出
  8. 使用模块化工具Rollup打包自己开发的JS库
  9. sql server行列转化和行列置换
  10. 为什么把持久化放到Domain Object是不OO的.
  11. 【第三课】ANR和OOM——贪快和贪多的后果(上)
  12. 阶段3 2.Spring_08.面向切面编程 AOP_10 总结和作业安排
  13. 【树叶分类】基于matlab GUI BP神经网络植物叶片识别分类【含Matlab源码 916期】
  14. PHP安装Xdebug调试工具
  15. easymule学习----校验dll信息
  16. 【项目需求】房屋租赁管理系统的分析与设计
  17. 求1-100的和的几种方法
  18. Go语言学习笔记(三)---指针,运算符及流程控制
  19. HTTP请求状态码集合
  20. VS2012 处理器架构“x86”不匹配 通过配置管理器更改您的项目的目标处理器架构...

热门文章

  1. julia在mac环境变量_在Julia中确定值/变量的类型
  2. js中的转译_JavaScript中的填充和转译
  3. c ++查找字符串_C ++类和对象| 查找输出程序| 套装1
  4. 聊聊并发编程的10个坑
  5. 超3000岗位!腾讯产业互联网新年大扩招!
  6. Ray.tune可视化调整超参数Tensorflow 2.0
  7. python+opencv实现机器视觉基础技术(2)(宽度测量,缺陷检测,医学检测
  8. wstring和string简单正则表达式使用
  9. EntityFramework进阶——Entity Splitting和Table Splitting
  10. leetCode —— 1200.最小绝对差