引导语

在 ArrayList 的类注释上,JDK 就提醒了我们,如果要把 ArrayList 作为共享变量的话,是线程不安全的,推荐我们自己加锁或者使用 Collections.synchronizedList 方法,其实 JDK 还提供了另外一种线程安全的 List,叫做 CopyOnWriteArrayList,这个 List 具有以下特征:

线程安全的,多线程环境下可以直接使用,无需加锁;
通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。

1 整体架构

从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:

  1. 加锁;
  2. 从原数组中拷贝出新数组;
  3. 在新数组上进行操作,并把新数组赋值给数组容器;
  4. 解锁

除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到,代码如下:

private transient volatile Object[] array;

整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。

1.1 类注释

我们看看从 CopyOnWriteArrayList 的类注释上能得到哪些信息:

  1. 所有的操作都是线程安全的,因为操作都是在新拷贝数组上进行的;
  2. 数组的拷贝虽然有一定的成本,但往往比一般的替代方案效率高;
  3. 迭代过程中,不会影响到原来的数组,也不会抛出 ConcurrentModificationException 异常。

接着我们来看下 CopyOnWriteArrayList 的核心方法源码。

2 新增

新增有很多种情况,比如说:新增到数组尾部、新增到数组某一个索引位置、批量新增等等,操作的思路还是我们开头说的四步,我们拿新增到数组尾部的方法举例,来看看底层源码的实现:

// 添加元素到数组尾部
public boolean add(E e) {final ReentrantLock lock = this.lock;// 加锁lock.lock();try {// 得到所有的原数组Object[] elements = getArray();int len = elements.length;// 拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素Object[] newElements = Arrays.copyOf(elements, len + 1);// 在新数组中进行赋值,新元素直接放在数组的尾部newElements[len] = e;// 替换掉原来的数组setArray(newElements);return true;// finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁   } finally {lock.unlock();}
}

从源码中,我们发现整个 add 过程都是在持有锁的状态下进行的,通过加锁,来保证同一时刻只能有一个线程能够对同一个数组进行 add 操作。

除了加锁之外,还会从老数组中创建出一个新数组,然后把老数组的值拷贝到新数组上,这时候就有一个问题:都已经加锁了,为什么需要拷贝数组,而不是在原来数组上面进行操作呢,原因主要为:

  1. volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
  2. 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响。

简单 add 操作是直接添加到数组的尾部,接着我们来看下指定位置添加元素的关键源码(部分源码):

// len:数组的长度、index:插入的位置
int numMoved = len - index;
// 如果要插入的位置正好等于数组的末尾,直接拷贝数组即可
if (numMoved == 0)newElements = Arrays.copyOf(elements, len + 1);
else {// 如果要插入的位置在数组的中间,就需要拷贝 2 次
// 第一次从 0 拷贝到 index。
// 第二次从 index+1 拷贝到末尾。newElements = new Object[len + 1];System.arraycopy(elements, 0, newElements, 0, index);System.arraycopy(elements, index, newElements, index + 1,numMoved);
}
// index 索引位置的值是空的,直接赋值即可。
newElements[index] = element;
// 把新数组的值赋值给数组的容器中
setArray(newElements);

从源码中可以看到,当插入的位置正好处于末尾时,只需要拷贝一次,当插入的位置处于中间时,此时我们会把原数组一分为二,进行两次拷贝操作。

最后还有个批量新增操作,源码我们就不贴了,底层也是拷贝数组的操作。

2.1 小结

从 add 系列方法可以看出,CopyOnWriteArrayList 通过加锁 + 数组拷贝+ volatile 来保证了线程安全,每一个要素都有着其独特的含义:

  1. 加锁:保证同一时刻数组只能被一个线程操作;
  2. 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改;
  3. volatile:值被修改后,其它线程能够立马感知最新值。

3 个要素缺一不可,比如说我们只使用 1 和 3 ,去掉 2,这样当我们修改数组中某个值时,并不会触发 volatile 的可见特性的,只有当数组内存地址被修改后,才能触发把最新值通知给其他线程的特性。

3 删除

接着我们来看下指定数组索引位置删除的源码:

// 删除某个索引位置的数据
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 {// 如果删除的数据在数组的中间,分三步走// 1. 设置新数组的长度减一,因为是减少一个元素// 2. 从 0 拷贝到数组新位置// 3. 从新位置拷贝到数组尾部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();}
}

步骤分为三步:

  1. 加锁;
  2. 判断删除索引的位置,从而进行不同策略的拷贝;
  3. 解锁。

代码整体的结构风格也比较统一:锁 + try finally +数组拷贝,锁被 final 修饰的,保证了在加锁过程中,锁的内存地址肯定不会被修改,finally 保证锁一定能够被释放,数组拷贝是为了删除其中某个位置的元素。

4 批量删除

数组的批量删除很有意思,接下来我们来看下 CopyOnWriteArrayList 的批量删除的实现过程:

// 批量删除包含在 c 中的元素
public boolean removeAll(Collection<?> c) {if (c == null) throw new NullPointerException();final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;// 说明数组有值,数组无值直接返回 falseif (len != 0) {// newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素int newlen = 0;Object[] temp = new Object[len];// 循环,把不包含在 c 里面的元素,放到新数组中for (int i = 0;i < len;++i) {Object element = elements[i];// 不包含在 c 中的元素,从 0 开始放到新数组中if (!c.contains(element))temp[newlen++] = element;}// 拷贝新数组,变相的删除了不包含在 c 中的元素if (newlen != len) {setArray(Arrays.copyOf(temp, newlen));return true;}}return false;} finally {lock.unlock();}
}

从源码中,我们可以看到,我们并不会直接对数组中的元素进行挨个删除,而是先对数组中的值进行循环判断,把我们不需要删除的数据放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。

不知道大家有木有似曾相识的感觉,ArrayList 的批量删除的思想也是和这个类似的,所以我们在需要删除多个元素的时候,最好都使用这种批量删除的思想,而不是采用在 for 循环中使用单个删除的方法,单个删除的话,在每次删除的时候都会进行一次数组拷贝(删除最后一个元素时不会拷贝),很消耗性能,也耗时,会导致加锁时间太长,并发大的情况下,会造成大量请求在等待锁,这也会占用一定的内存。

5 其它方法

5.1 indexOf

indexOf 方法的主要用处是查找元素在数组中的下标位置,如果元素存在就返回元素的下标位置,元素不存在的话返回 -1,不但支持 null 值的搜索,还支持正向和反向的查找,我们以正向查找为例,通过源码来说明一下其底层的实现方式:

// o:我们需要搜索的元素
// elements:我们搜索的目标数组
// index:搜索的开始位置
// fence:搜索的结束位置
private static int indexOf(Object o, Object[] elements,int index, int fence) {// 支持对 null 的搜索if (o == null) {for (int i = index;i < fence;i++)// 找到第一个 null 值,返回下标索引的位置if (elements[i] == null)return i;} else {// 通过 equals 方法来判断元素是否相等// 如果相等,返回元素的下标位置for (int i = index;i < fence;i++)if (o.equals(elements[i]))return i;}return -1;
}

indexOf 方法在 CopyOnWriteArrayList 内部使用也比较广泛,比如在判断元素是否存在时(contains),在删除元素方法中校验元素是否存在时,都会使用到 indexOf 方法,indexOf 方法通过一次 for 循环来查找元素,我们在调用此方法时,需要注意如果找不到元素时,返回的是 -1,所以有可能我们会对这个特殊值进行判断。

5.2 迭代

在 CopyOnWriteArrayList 类注释中,明确说明了,在其迭代过程中,即使数组的原值被改变,也不会抛出 ConcurrentModificationException 异常,其根源在于数组的每次变动,都会生成新的数组,不会影响老数组,这样的话,迭代过程中,根本就不会发生迭代数组的变动,我们截几个图说明一下:

  1. 迭代是直接持有原有数组的引用,也就是说迭代过程中,一旦原有数组的值内存地址发生变化,必然会影响到迭代过程,下图源码演示的是 CopyOnWriteArrayList 的迭代方法,我们可以看到迭代器是直接持有原数组的引用:
  2. 我们写了一个 demo,在 CopyOnWriteArrayList 迭代之后,往 CopyOnWriteArrayList 里面新增值,从下图中可以看到在 CopyOnWriteArrayList 迭代之前,数组的内存地址是 962,请记住这个数字:
  3. CopyOnWriteArrayList 迭代之后,我们使用 add(“50”) 代码给数组新增一个数据后,数组内存地址发生了变化,内存地址从原来的 962 变成了 968,这是因为 CopyOnWriteArrayList 的 add 操作,会生成新的数组,所以数组的内存地址发生了变化:
  4. 迭代继续进行时,我们发现迭代器中的地址仍然是迭代之前引用的地址,是 962,而不是新的数组的内存地址:

    从上面 4 张截图,我们可以得到迭代过程中,即使 CopyOnWriteArrayList 的结构发生变动了,也不会抛出 ConcurrentModificationException 异常的原因:CopyOnWriteArrayList 迭代持有的是老数组的引用,而 CopyOnWriteArrayList 每次的数据变动,都会产生新的数组,对老数组的值不会产生影响,所以迭代也可以正常进行。

6 总结

当我们需要在线程不安全场景下使用 List 时,建议使用 CopyOnWriteArrayList,CopyOnWriteArrayList 通过锁 + 数组拷贝 + volatile 之间的相互配合,实现了 List 的线程安全,我们抛弃 Java 的这种实现,如果让我们自己实现,你又将如何实现呢?

老猿说说-CopyOnWriteArrayList相关推荐

  1. 老猿学5G专栏完结说明

    老猿学5G是因为工作原因促成的,主要目的是为了研究5G的计费架构相关内容,到今天为止,基本上达成目标,因此这个专栏基本上告一段落了. 回想这2个多月的日子,从一个对5G相关知识完全不熟悉的小白,到现在 ...

  2. 为什么用python写爬虫_老猿为什么写Python爬虫教程

    对于"爬虫", 或许你只是听说过,或许已经有所了解.无论怎样,你可能有过这样的困惑: + 学了爬虫不知道怎么挣钱? + 技术不知道如何进阶? + 遇到问题不知道找谁交流? 十多年前 ...

  3. 70后.net老猿,尚能饭否?

    相关阅读: 阿里P7/P8学习路线图--技术封神之路 MySQL大表优化方案,单表优化.读写分离.缓存.分区表--都在这里了 互联网技术(java框架.分布式.集群)干货视频大全,不看后悔!(免费下载 ...

  4. Blender基础建模 | 大帅老猿threejs特训

    最近接触到了web3d相关的知识,特别感觉"大帅老猿"和"胖达老师"带我入门! 今天来和大家一起讲讲我建模的故事!顺便教大家一起实现建模自由! 一.首先下载一个 ...

  5. 老猿学5G扫盲贴:中国移动网络侧CHF主要功能及计费处理的主要过程

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一. ...

  6. 《老猿Python精品文章》专栏文章目录

    ☞ ░ 前往老猿Python博客 https://blog.csdn.net/LaoYuanPython ░ 一.专栏简介 <老猿Python精品文章>专栏收集的是老猿2021年开始写的一 ...

  7. 老猿学5G扫盲贴:N6接口用户平面协议栈对应的网络分层模型

    在网络通信模型中,都对应有分层的网络结构,如开放式系统互联(OSI)的七层模型(物理层.数据链路层.网络层.传输层.会话层.表示层和应用层)以及TCP/IP四层(网络接口层.网络层.传输层和应用层)模 ...

  8. 老猿学5G扫盲贴:移动边缘计算(Mobile Edge Computing, MEC)

    版权声明:本文为CSDN博主「魏晓蕾」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csdn.net/gongxifaca ...

  9. 老猿学5G扫盲贴:与用户和终端相关的名词UE、SUPI、GPSI、PEI

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 和4 ...

最新文章

  1. 尺度空间理论与图像金字塔(二)
  2. NYOJ 单调递增子序列(二)
  3. SpringBoot专栏(一) -- SpringBoot简介
  4. [转]蓝牙基带数据传输机理分析
  5. 从手机App到网点交互请求的日志记录
  6. 简述java在安卓开发中的应用_Java 自定义注解在安卓开发中的简单运用
  7. 机器学习之朴素贝叶斯算法原理
  8. spring mvc Spring Data Redis RedisTemplate [转]
  9. neo4j docker
  10. jupyter notebook 修改主题、字体、字号等
  11. 77GHz汽车防撞雷达信号处理设计与实现
  12. dsoframer java_基于DsoFramer控件的Office编辑控件
  13. Web前端布局总体架构
  14. 3 整型变量,实型数据(主要是实型变量)2021-01-25
  15. html悬浮客服代码,js QQ客服悬浮效果实现代码
  16. U盘容量变小实用解决方案
  17. 接口设计需要考虑哪些方面
  18. 新手lbw + 《软件工程(C编码实践篇)》MOOC课程作业http://mooc.study.163.com/course/USTC-1000002006
  19. 首批预计才60万套。 四轮猴
  20. 软件许可证管理应该怎么做?

热门文章

  1. 学习雷锋精神践行新时代志愿活动
  2. 点云配准新方案!SuperLine3D:激光雷达点云中的自监督线分割和描述子提取(ECCV2022)...
  3. 黔南计算机职称考试报名,2017年黔南职称计算机考试报名通知
  4. 不要去衡量风险本身,去衡量风险爆发之后的结果
  5. latch和lock的区别
  6. EF的水印和验证码图制做
  7. Linux基础知识汇总
  8. 智慧医疗基础平台-03
  9. 【CF #790 D. X-Sum】正反对角线和模拟
  10. 低成本人体红外+环境光感应的可充电小夜灯电路及PCB设计