原文链接:https://www.changxuan.top/?p=1252


CopyOnWriteArrayList 是 JUC 中唯一一个支持并发的 List。

CopyOnWriteArrayList 的修改操作都是在底层的一个复制的数组上进行,即写时复制策略,从而实现了线程安全。其实原理和数据库的读写分离十分相似。

基本构成

底层使用数组 private transient volatile Object[] array; 来存储元素,使用 ReentrantLock 独占锁保证相关操作的安全性。

构造函数

public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 将集合 c 内的元素复制到 list 中
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
// 创建一个内部元素是 toCopyIn 副本的 list
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

添加元素

CopyOnWriteArrayList 中与添加元素相关的方法有以下几种:

  • add(E e)
  • add(int index, E element)
  • addAll(Collection<? extends E> c)
  • addAll(int index, Collection<? extends E> c)
  • addIfAbsent(E e)
  • addAllAbsent(Collection<? extends E> c)

鉴于原理基本相似,下面只分析 add(E e)addIfAbsent(E e) 方法做为例子。

add(E e)

源码

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 方法的线程首先会去尝试获取独占锁,成功获取的线程会继续执行后续添加元素逻辑,而未获取独占锁的线程在没有异常的情况下则会阻塞挂起。等待独占锁被释放后,再次尝试获取。(ps. 在 CopyOnWriteArrayList 中使用的是 ReentrantLock 的非公平锁模式)

这样就能保证,同一时间只有一个线程进行添加元素。

addIfAbsent(E e)

源码

public boolean addIfAbsent(E e) {
// 获取当前数组
Object[] snapshot = getArray();
// 调用 indexOf 判断元素是否已存在 (遍历)
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
} private boolean addIfAbsent(E e, Object[] snapshot) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 再次获取当前数组
Object[] current = getArray();
int len = current.length;
// 此处判断是检查在当前线程判断元素不存在和获取独占锁之间的这段时间内是否有其它线程对数组进行了更改操作
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
// 与 add 方法的逻辑相同
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

如果当前数组中不存在元素 eaddIfAbsent 则会将 e 添加至数组中返回 true;如果当前数组中存在待添加元素,则会返回 false

addIfAbsent 方法中为了提高性能,设计者把判断“当前数组是否存在待添加元素”和“添加元素”的操作分开了。由于前一个操作不必要获取独占锁,在遇到每次待添加的元素都已经存在于数组的情况时可以高效的返回 false

因为上面提到的两步操作是非原子性的,所以再第二步操作中还需要再次进行确认之前用来判断不存在元素 e 的数组是否被“掉包”了。如果被“掉包”,那么也不要“嫌弃”。就需要再判断一下“掉包”后的数组还能不能接着用。如果不能用直接返回 false,如果发现能用就继续向下执行,成功后返回 true

这种设计思路,在自己的业务系统中还是比较值的借鉴的。当然上述场景下“坏”的设计,就是会先尝试获取独占锁,在获取独占锁后再进行“判断元素是否存在和决定是否添加元素的操作”。这样则会导致大大增加线程阻塞挂起几率。相信大多数同学还是能写出漂亮的代码的,不至于犯这种小错误。

获取元素

获取元素一共涉及到三个方法,源码如下:

public E get(int index) {
return get(getArray(), index);
}
// 步骤一
final Object[] getArray() {
return array;
}
// 步骤二
private E get(Object[] a, int index) {
return (E) a[index];
}

我们看到获取元素的操作,全程没有加锁。并且获取元素是由两步操作组合而成的,一获取当前数组,二从当前数据中取出所指定的下标位置的元素。一旦在这两步操作之间,有其它线程更改了 index 下标位置的元素。此时,获取元素的线程所使用的数组则是被废弃掉的,它接收到的值也不是最新的值。这是写时复制策略产生的弱一致性问题

修改元素

可以使用 set(int index, E element) 方法修改指定位置的元素。

源码

public E set(int index, E element) {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前数组
Object[] elements = getArray();
// 获取要修改位置的元素
E oldValue = get(elements, index);
// 新值与老值是否一样
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}

可以看到,在代码中并没有显示的判断 index 是否合法,如果不合法则会抛出 IndexOutOfBoundsException 异常。

主要逻辑也是先尝试获取独占锁,符合条件则进行修改。需要注意的一点是,如果指定索引处的元素值与新值相等,也会调用 setArray(Object[] a) 一次方法,这主要是为了保证 volatile 语义。(线程在写入 volatile 变量时,不会把值缓存在寄存器或者其它地方,而是会把值刷回到主内存,确保内存可见性)

删除元素

删除元素的方法包括:

  • E remove(int index)
  • boolean remove(Object o)
  • boolean removeAll(Collection<?> c)

我们来看下 remove(int index) 的实现,

源码

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();
}
}

其实代码逻辑很清楚,获取锁后根据情况复制老数组中的未删除数据到新数组即可。

迭代器

不知道大家有没有在遍历 ArrayList 变量的过程中想没想过删除其中的某个元素?反正我曾经这么写过,然后就出现了问题 ... 后来使用了 ArrayList 的迭代器之后就没有错误了。

CopyOnWriteArrayList 中也有迭代器,但是也存在着弱一致性问题

源码

public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
} static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array 数组的快照版本 */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
// 构造函数
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 是否结束
public boolean hasNext() {
return cursor < snapshot.length;
} public boolean hasPrevious() {
return cursor > 0;
}
// 获取元素
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
} ... ... public int nextIndex() {
return cursor;
} public int previousIndex() {
return cursor-1;
} public void remove() {
throw new UnsupportedOperationException();
} public void set(E e) {
throw new UnsupportedOperationException();
} public void add(E e) {
throw new UnsupportedOperationException();
} ... ...
}

可以看到,CopyOnWriteArrayList 的迭代器并不支持 remove 操作。在调用 iterator() 方法时获取了一份当前数组的快照,如果在遍历期间并没有其它线程对数据做更改操作就不会出现一致性的问题。一旦有其它线程对数据更改后,将 CopyOnWriteArrayList 中的数组更改为了新数组,此时迭代器所持有的数据就相当于快照了,同时也出现了弱一致性问题。

拓展延申

还记得刚刚提到的 addIfAbsent 方法吗?看到它你有没有联想到什么东西呢?集合 set?

对的,通过 addIfAbsent 方法也能实现集合的功能,CopyOnWriteArraySet 的底层就是使用 CopyOnWriteArrayList 实现的。(PS. HasSet 的底层依赖 HashMap 。)

剖析 CopyOnWriteArrayList的更多相关文章

  1. 并发编程之 CopyOnWriteArrayList 源码剖析

    前言 ArrayList 是一个不安全的容器,在多线程调用 add 方法的时候会出现 ArrayIndexOutOfBoundsException 异常,而 Vector 虽然安全,但由于其 add ...

  2. MapReduce剖析笔记之二:Job提交的过程

    上一节以WordCount分析了MapReduce的基本执行流程,但并没有从框架上进行分析,这一部分工作在后续慢慢补充.这一节,先剖析一下作业提交过程. 在分析之前,我们先进行一下粗略的思考,如果要我 ...

  3. ArrayList源码剖析

    ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...

  4. 转:【Java集合源码剖析】ArrayList源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/35568011   本篇博文参加了CSDN博文大赛,如果您觉得这篇博文不错,希望您能帮我投一 ...

  5. CopyOnWriteArrayList你都不知道,怎么拿offer?

    前言 只有光头才能变强 前一阵子写过一篇COW(Copy On Write)文章,结果阅读量很低啊...COW奶牛!Copy On Write机制了解一下 可能大家对这个技术比较陌生吧,但这项技术是挺 ...

  6. Spark源码剖析 - SparkContext的初始化(三)_创建并初始化Spark UI

    3. 创建并初始化Spark UI 任何系统都需要提供监控功能,用浏览器能访问具有样式及布局并提供丰富监控数据的页面无疑是一种简单.高效的方式.SparkUI就是这样的服务. 在大型分布式系统中,采用 ...

  7. 并发编程之 ConcurrentLinkedQueue 源码剖析

    前言 今天我们继续分析 java 并发包的源码,今天的主角是谁呢?ConcurrentLinkedQueue,上次我们分析了并发下 ArrayList 的替代 CopyOnWriteArrayList ...

  8. 【Java集合源代码剖析】ArrayList源代码剖析

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/mmc_maodun/article/details/35568011 转载请注明出处:http:// ...

  9. 性能测试工具 nGrinder 项目剖析及二次开发

    转:https://testerhome.com/topics/4225 0.背景 组内需要一款轻量级的性能测试工具,之前考虑过LR(太笨重,单实例,当然它的地位是不容置疑的),阿里云的PTS(htt ...

随机推荐

  1. 学习Promise异步编程

    JavaScript引擎建立在单线程事件循环的概念上.单线程( Single-threaded )意味着同一时刻只能执行一段代码.所以引擎无须留意那些"可能"运行的代码.代码会被放 ...

  2. java Swing组件随着窗口拖动等比移动或等比放大

    实现原理很简单, 1清空布局(使用绝对布局) 2添加监听器(监听窗口是否被拖动) 3在监听器里面动态调整 组件的位置 效果如下: 拖动之后效果: 代码实现: import java.awt.Event ...

  3. Python 爬虫系列

    爬虫简介 网络爬虫 爬虫指在使用程序模拟浏览器向服务端发出网络请求,以便获取服务端返回的内容. 但这些内容可能涉及到一些机密信息,所以爬虫领域目前来讲是属于灰色领域,切勿违法犯罪. 爬虫本身作为一门技 ...

  4. Android——几种数据存储应用浅谈

    (1)android中的数据存储主要有五种方式: 第一种.sharedPreferences存储数据, 适用范围:保存少量的数据,且这些数据的格式非常简单:字符串型.基本类型的值.比如应用程序的各种配 ...

  5. B树与B+树区别辨析

    我们都知道,innodb中的索引结构使用的是B+树.B+树是一种B树的变形树,而B树又是来源于平衡二叉树.相较于平衡二叉树,B树更适合磁盘场景下文件索引系统.那为什么B树更适合磁盘场景,B+树又在B树 ...

  6. Win 10 Docker安装和简单使用

    Win 10 Docker安装和简单使用 1.环境准备 Docker for Windows需要运行在64位Windows 10 Pro专业版.企业版或教育版(1607年纪念更新,版本14393或更高 ...

  7. MyBatis 查询数据时属性中多对一的问题(多条数据对应一条数据)

    数据准备 数据表 CREATE TABLE `teacher`( id INT(10) NOT NULL, `name` VARCHAR(30) DEFAULT NULL, PRIMARY KEY ( ...

  8. 【设计模式】Java设计模式精讲之原型模式

    简单记录 - 慕课网 Java设计模式精讲 Debug方式+内存分析 & 设计模式之禅-秦小波 文章目录 1.原型模式的定义 原型-定义 原型-类型 2.原型模式的实现 原型模式的通用类图 原 ...

  9. 【Linux】nginx详细说明

    Nginx的配置文件nginx.conf配置详解如下: user nginx nginx ; Nginx用户及组:用户 组.window下不指定 worker_processes 8; 工作进程:数目 ...

  10. Redis 实战 —— 01. Redis 数据结构简介

    一些数据库和缓存服务器的特性和功能 P4 名称 类型 数据存储选项 查询类型 附加功能 Redis 使用内存存储(in-memory)的非关系数据库 字符串.列表.哈希表.集合.有序集合 每种数据类型 ...