CopyOnWriteArrayList底层是怎么保证线程安全的?

欢迎学习解读JAVA源码专栏,在这个系列中,我将手把手带着大家剖析Java核心组件的源码,内容包含集合、线程、线程池、并发、队列等,深入了解其背后的设计思想和实现细节,轻松应对工作面试 。
这是解读Java源码系列的第三篇 , 将跟大家一起学习比较神秘的并发集合 - CopyOnWriteArrayList 。
引言上篇文章提到ArrayList不是线程安全的,而CopyOnWriteArrayList是线程安全的 。此刻我就会产生几个问题:

  1. CopyOnWriteArrayList初始容量是多少?
  2. CopyOnWriteArrayList是怎么进行扩容的?
  3. CopyOnWriteArrayList是怎么保证线程安全的?
带着这几个问题,一起分析一下CopyOnWriteArrayList的源码 。
简介CopyOnWriteArrayList是一种线程安全的ArrayList,底层是基于数组实现,不过该数组使用了volatile关键字修饰 。实现线程安全的原理是,“人如其名”,就是 Copy On Write(写时复制) , 意思就是在对其进行修改操作的时候 , 复制一个新的ArrayList , 在新的ArrayList上进行修改操作 , 从而不影响旧的ArrayList的读操作 。看一下源码中CopyOnWriteArrayList内部有哪些数据结构组成:
public class CopyOnWriteArrayList<E>    implements List<E>, Randomaccess, Cloneable, java.io.Serializable {    // 加锁 , 用来保证线程安全    final transient ReentrantLock lock = new ReentrantLock();    // 存储元素的数组 , 使用了volatile修饰    private transient volatile Object[] array;    // 数组的get/set方法    final Object[] getArray() {        return array;    }    final void setArray(Object[] a) {        array = a;    }}CopyOnWriteArrayList的内部结构非常简单 , 使用ReentrantLock加锁,用来保证线程安全 。使用数组存储元素,数组使用volatile修饰,用来保证内存可见性 。当其他线程重新对数组对象进行赋值的时候,当前线程可以及时感知到 。
初始化当我们调用CopyOnWriteArrayList的构造方法的时候,底层逻辑是怎么实现的?
List<Integer> list = new CopyOnWriteArrayList<>();CopyOnWriteArrayList初始化的时候,不支持指定数组长度,接着往下看,就能明白CopyOnWriteArrayList为什么不支持指定数组长度 。
public CopyOnWriteArrayList() {    setArray(new Object[0]);}初始化过程非常简单,就是创建了一个长度为0的数组 。
添加元素再看一下往CopyOnWriteArrayList添加元素时,调用的 add() 方法源码实现:
// 添加元素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 {        // 释放锁        lock.unlock();    }}添加元素的流程:
  1. 先使用ReentrantLock加锁 , 保证线程安全 。
  2. 再创建一个新数组,长度是原数组长度+1,并把原数组元素拷贝到新数组里面 。
  3. 然后在新数组末尾位置赋值
  4. 使用新数组替换掉原数组


    推荐阅读