前言

JUC 下面还有一个系列的类,都是 CopyOnWriteXXX ,意思是写时复制,这个究竟是怎么回事?那就以 CopyOnWriteArrayList 为切入点,一起了解写时复制是怎么回事?

公众号:liuzhihangs,记录工作学习中的技术、开发及源码笔记;时不时分享一些生活中的见闻感悟。欢迎大佬来指导!

介绍

ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。

像名字一样,每次进行操作的时候,都会进行一次复制,当然会有很大的性能消耗,但是在某些使用场景下,又会提高性能。具体是怎么操作的,那就一步一步阅读源码,然后再做总结归纳。

基本使用

  1. public class CopyOnWriteArrayListTest {
  2. public static void main(String[] args) {
  3. CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
  4. // 添加元素
  5. list.add("liuzhihang");
  6. // 移除元素
  7. list.remove("liuzhihang");
  8. // 查看元素
  9. String value0 = list.get(0);
  10. // 遍历
  11. Iterator<String> iterator = list.iterator();
  12. while (iterator.hasNext()) {
  13. String next = iterator.next();
  14. }
  15. }
  16. }

问题疑问

  1. 为什么要叫写时复制集合?
  2. CopyOnWriteArrayList 实现原理是什么?
  3. CopyOnWriteArrayList 和 ArrayList 有什么区别?
  4. CopyOnWriteArrayList 复制是怎么进行复制的?

源码分析

基本结构

参数介绍

  1. public class CopyOnWriteArrayList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  3. private static final long serialVersionUID = 8673264195747942595L;
  4. /** 数据有变动时使用 */
  5. final transient ReentrantLock lock = new ReentrantLock();
  6. /** 数组 只能通过 getArray/setArray 访问 */
  7. private transient volatile Object[] array;
  8. final Object[] getArray() {
  9. return array;
  10. }
  11. // 将数组指向传入的新数组
  12. final void setArray(Object[] a) {
  13. array = a;
  14. }
  15. }

通过参数可以了解到以下内容:

  1. 基于数组实现;
  2. 使用了 ReentrantLock 互斥锁。

构造函数

  1. public CopyOnWriteArrayList() {
  2. setArray(new Object[0]);
  3. }

在初始化 CopyOnWriteArrayList 时,就是创建了一个 Object 的数组。

add

  1. public boolean add(E e) {
  2. // 加锁
  3. final ReentrantLock lock = this.lock;
  4. lock.lock();
  5. try {
  6. // 获取当前数组
  7. Object[] elements = getArray();
  8. int len = elements.length;
  9. // 创建一个新数组,并将原数组数据复制到新数组
  10. Object[] newElements = Arrays.copyOf(elements, len + 1);
  11. // 添加的新元素到数组尾部
  12. newElements[len] = e;
  13. // 将数组指向新数组
  14. setArray(newElements);
  15. return true;
  16. } finally {
  17. lock.unlock();
  18. }
  19. }

add 方法逻辑很简单:

  1. 通过加互斥锁(ReentrantLock)从而保证在写的时候只有一个线程可以写。
  2. 新增元素时,先使用 Arrays.copyOf(elements, len + 1) 复制出一个长度 +1 的新数组。
  3. 添加元素到新数组。
  4. 然后再将原数组对象指向新数组。

画图如下:

remove

  1. public E remove(int index) {
  2. // 加锁
  3. final ReentrantLock lock = this.lock;
  4. lock.lock();
  5. try {
  6. // 原数组
  7. Object[] elements = getArray();
  8. int len = elements.length;
  9. // 移除的值
  10. E oldValue = get(elements, index);
  11. int numMoved = len - index - 1;
  12. if (numMoved == 0)
  13. // 如果移除最后一个元素
  14. // 直接复制前面的元素即可
  15. setArray(Arrays.copyOf(elements, len - 1));
  16. else {
  17. // 移除中间的元素,进行两次复制
  18. Object[] newElements = new Object[len - 1];
  19. System.arraycopy(elements, 0, newElements, 0, index);
  20. System.arraycopy(elements, index + 1, newElements, index,
  21. numMoved);
  22. setArray(newElements);
  23. }
  24. return oldValue;
  25. } finally {
  26. lock.unlock();
  27. }
  28. }

remove 方法相对多了一些判断:

  1. 通过加互斥锁(ReentrantLock)从而保证在写的时候只有一个线程可以移除元素。
  2. 如果移除的是最后一个元素,则直接复制前面的元素到新数组,并指向新数组即可。
  3. 如果移除的是中间的元素,则需要进行两次复制,然后指向新数组。

画图如下:

get

  1. public E get(int index) {
  2. return get(getArray(), index);
  3. }

get 方法可以看出:

  1. 获取元素并没有进行加锁。
  2. 从原数组获取的元素。

所以并发情况下,并不能保证很及时的读取的刚插入或者移除的元素。

数组复制

通过阅读 add 和 remove 相关代码,可以看到在数组复制时使用了 Arrays.copyOfSystem.arraycopy,这相当于一个优化方面吧。

毕竟数组复制总不能把原数组遍历一遍,挨着赋值到新数组里面吧。

那接下来看一下内部是如何实现的:

  1. public class Arrays {
  2. // 其他方法省略 ...
  3. /**
  4. * original 要复制的数组
  5. * newLength 新数组的长度
  6. */
  7. public static <T> T[] copyOf(T[] original, int newLength) {
  8. return (T[]) copyOf(original, newLength, original.getClass());
  9. }
  10. /**
  11. * original 要复制的数组
  12. * newLength 新数组的长度
  13. */
  14. public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  15. // 创建一个新数组,长度是指定的长度
  16. @SuppressWarnings("unchecked")
  17. T[] copy = ((Object)newType == (Object)Object[].class)
  18. ? (T[]) new Object[newLength]
  19. // 创建具有指定的组件类型和长度的新数组
  20. : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  21. // 调用 System.arraycopy 复制数组
  22. System.arraycopy(original, 0, copy, 0,
  23. Math.min(original.length, newLength));
  24. return copy;
  25. }
  26. }

通过阅读 Arrays.copyOf 相关源码,发现其实 Arrays.copyOf 底层也是调用的 System.arraycopy

  1. public final class System {
  2. // 其他方法省略 ...
  3. /**
  4. * src 源数组
  5. * srcPos 源数组起始位置
  6. * dest 目标数组
  7. * destPos 目标数组的起始位置
  8. * length 要复制的元素数量
  9. */
  10. public static native void arraycopy(Object src, int srcPos,
  11. Object dest, int destPos,
  12. int length);
  13. }

可以看到 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的更多相关文章

  1. JAVA中写时复制(Copy-On-Write)Map实现

    1,什么是写时复制(Copy-On-Write)容器? 写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改.修改 ...

  2. Java进阶知识点6:并发容器背后的设计理念 - 锁分段、写时复制和弱一致性

    一.背景 容器是Java编程中使用频率很高的组件,但Java默认提供的基本容器(ArrayList,HashMap等)均不是线程安全的.当容器和多线程并发编程相遇时,程序员又该何去何从呢? 通常有两种 ...

  3. php 内核变量 引用计数器写时复制

    写时复制,是一个解决内存复用的方法,就是你在php语言层,如$d=$c=$b=$a='value';把$a赋给另一个或多个变量,这时这个变量都只占用一个内存块,当其中一个变量值改变时,才会开辟另一个内 ...

  4. 线程不安全(Arraylist,HashSet,HashMap)和写时复制

    package com.yangyuanyuan.juc1205; import java.util.List; import java.util.Map; import java.util.Set; ...

  5. phpCOW机制(写时复制)

    写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改. COW最早应用在*nix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中,如C ...

  6. php变量 写时改变 写时复制

    写时复制 $var = 1; $var2 = $var; #此时$var2 与 $var 指向同一个zval refcount = 2: $var = 2; # 此时$val 改变 所以 $var 与 ...

  7. PHP写时复制, 变量复制和对象复制不同!!!

    2016年3月18日 15:09:28 星期五 一直以为PHP对象也是写时复制....... 其实: PHP的变量是写时复制, 对象是引用的 写时复制: $a = $b; 如果$b的内容不改变, $a ...

  8. php 垃圾回收机制----写时复制和引用计数

    PHP使用引用计数和写时复制来管理内存.写时复制保证了变量间复制值不浪费内存,引用计数保证了当变量不再需要时,将内存释放给操作系统. 要理解PHP内存管理,首先要理解一个概念----符号表. 符号表的 ...

  9. fork()和写时复制

    写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork(  )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程.这种行为是非常耗时的,因为它需要: · ...

随机推荐

  1. 049 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 11 break语句

    049 01 Android 零基础入门 01 Java基础语法 05 Java流程控制之循环结构 11 break语句 本文知识点:break语句 break语句 break语句前情回顾 1.swi ...

  2. C++ 异常处理 catch(...)介绍

    转载:https://blog.csdn.net/fcsfcsfcs/article/details/77717567 catch(-)能够捕获多种数据类型的异常对象,所以它提供给程序员一种对异常 对 ...

  3. HTML & CSS & JavaScript 从一个表格到一个灰阶颜色表 01

    工具1:HBuilder X 1.9.9.20190522 工具2:火狐浏览器 67.0.4 (64 位) 在开始之前,你需要了解一些基本的知识,不要求你能掌握,但是了解一下还是有益的: HTML 简 ...

  4. 【题解】[SHOI2007]善意的投票 / [JLOI2010]冠军调查

    Link \(\text{Solution:}\) 我们令源点和汇点分别为睡觉和不睡觉这两种互斥的决策点.把小朋友看成点,问题转化为最小割. 每一个小朋友对自己的意愿指向的汇点/源点.容量为\(1.\ ...

  5. 十一、模拟扫码登录微信(用Django简单的布置了下页面)发送接收消息

    为了能够模拟登陆QQ,并获取信息.对扫码登录微信进行了分析.简单的用了一下Django将获取的信息映射到页面上.(python3+pycharm) 主要过程就是: 1.获取二维码 2.扫码登录(有三种 ...

  6. NOIP提高组2013 D2T3 【华容道】

    某王  老师给我们考了一场noip2013的真题...心态爆炸! 题目大意: 有一个n*m的棋盘,每个格子上都有一个棋子,有些格子上的棋子能够移动(可移动的棋子是固定的),棋盘中有一个格子是空的,仍何 ...

  7. RHSA-2018:0151-重要: 内核 安全和BUG修复更新(需要重启、存在EXP、本地提权)

    [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...

  8. Linux操作系统(第二版)(RHEL 8/CentOS 8)

    Linux操作系统(第二版)(RHEL 8/CentOS 8) http://www.tup.tsinghua.edu.cn/booksCenter/book_08172501.html Linux操 ...

  9. Python基础笔记2-ruamel.yaml读写yaml文件

    上一篇笔记记录了Python中的pyyaml库对yaml文件进行读写,但了解到ruamel.yaml也能对yaml文件进行读写,于是想尝试一下它的用法. 一,注意 这里首先要更正一下网上大部分博客的说 ...

  10. Python初学习:简单的练习题

    Python初学习 一些见到那的练习题: 初级难度 设计一重量转换器,输入以g为单位的数字后,返回换算结果以Kg为单位的结果 中级难度 设计一个求直角三角形斜边长的函数,(以两个直角边为参数,求最长边 ...