2025-06-12
内卷九股文
0

目录

CopyOnWriteArrayList
核心设计思想
源码深度解析
读操作
写操作
移除数据
覆盖数据&清空集合
迭代器
总结

我们都知道在高并发下 ArrayList 是线程不安全的,那如果要使用集合要如何处理呢?接下来即将深入探讨Java并发容器中的 CopyOnWriteArrayList。见名知义,包含 Array 的底层还是通过数组实现的,这种特别适合读多写少的并发环境,接下来我们去深入了解下 CopyOnWriteArrayList 是如何能在高并发下保证数据的一致性的。

CopyOnWriteArrayList

CopyOnWriteArrayList(COW)是一个线程安全的ArrayList,采用 "写时复制" 思想,实现了读操作完全无锁的高性能并发容器。

核心设计思想

CopyOnWriteArrayList是基于 ReentrantLock 锁和 数组副本 的形式去保证线程安全。

读取 读取:直接访问当前数组,无需加锁

写入 在写数据时,先获取lock锁,复制原数组创建一个副本数组,在新数组上执行修改操作,将副本数组赋值给CopyOnWriteArrayList中的array(因为是volatile的,原子替换引用指向新数组)。

写操作性能开销(内存复制+GC压力)

因为CopyOnWriteArrayList每次写数据都要构建一个副本,如果你的业务是写多,并且数组中的数据量比较大,尽量避免去使用CopyOnWriteArrayList,因为这里会构建大量的数组副本,比较占用内存资源。

CopyOnWriteArrayList是弱一致性的,写操作先执行,但是副本还有落到CopyOnWriteArrayList的array属性中,此时读操作是无法查询到的。

java
// 伪代码演示COW机制 public void add(E element) { Object[] oldArray = currentArray; // 1. 获取当前数组 Object[] newArray = Arrays.copyOf(oldArray, oldArray.length + 1); // 2. 复制新数组 newArray[newArray.length - 1] = element; // 3. 修改新数组 currentArray = newArray; // 4. 原子替换引用 }

源码深度解析

主要查看2个核心属性,以及2个核心方法,还有无参构造

java
/** 写操作时,需要先获取到的锁资源,CopyOnWriteArrayList全局唯一的。 */ final transient ReentrantLock lock = new ReentrantLock(); /** 保证可见性的数组引用 (volatile保证写操作对其他线程立即可见) CopyOnWriteArrayList真实存放数据的位置,查询也是查询当前array */ private transient volatile Object[] array; // 获取当前数组属性 final Object[] getArray() { return array; } // 替换array属性 final void setArray(Object[] a) { array = a; } /** * 默认new的CopyOnWriteArrayList数组长度为0。 * 不像ArrayList,初始长度是10,每次扩容1/2, CopyOnWriteArrayList不存在这个概念 * 每次写的时候都会构建一个新的数组 */ public CopyOnWriteArrayList() { setArray(new Object[0]); }

volatile + 锁 双重保障

  • volatile 保证数组引用的可见性
  • ReentrantLock 保证写操作的原子性

读操作

CopyOnWriteArrayList的读操作就是get方法,基于数组索引位置获取数据。

方法之所以要差分成两个,是因为CopyOnWriteArrayList中在获取数据时,不单单只有一个array的数组需要获取值,还有副本中数据的值。

java
// 查询数据时,只能通过get方法查询CopyOnWriteArrayList中的数据 public E get(int index) { // 直接访问当前数组,调用get方法的重载 return get(getArray(), index); } // 执行get(int)时,内部调用的方法 private E get(Object[] a, int index) { // 直接拿到数组上指定索引位置的值,无锁读取数据 return (E) a[index]; }

写操作

CopyOnWriteArrayList是基于lock锁和副本数组的形式保证线程安全。

java
// 写入元素,不指定索引位置,直接放到最后的位置 public boolean add(E e) { // 获取全局锁,并执行lock 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; // 将副本数组,赋值给CopyOnWriteArrayList的原数组 setArray(newElements); // 添加成功,返回true return true; } finally { // 释放锁~ lock.unlock(); } } // 写入元素,指定索引位置。(不会覆盖数据) public void add(int index, E element) { // 拿锁,加锁~ final ReentrantLock lock = this.lock; lock.lock(); try { // 获取原数组,还获取了原数组的长度 Object[] elements = getArray(); int len = elements.length; // 如果索引位置大于原数组的长度,或者索引位置是小于0的。 if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); // 声明了副本数组 Object[] newElements; // 原数组长度 - 索引位置等到numMoved int numMoved = len - index; // 如果numMoved为0,说明数据要放到最后面的位置 if (numMoved == 0) // 直接走了原生态的方式,正常复制一份副本数组 newElements = Arrays.copyOf(elements, len + 1); else { // 数组要插入的位置不是最后一个位置 // 副本数组长度依然是原长度 + 1 newElements = new Object[len + 1]; // 将原数组从0索引位置开始复制,复制到副本数组中的前置位置 System.arraycopy(elements, 0, newElements, 0, index); // 将原数组从index位置开始复制,复制到副本数组的index + 1往后放。 // 这时,index就空缺出来了。 System.arraycopy(elements, index, newElements, index + 1, numMoved); } // 数据正常放到指定的索引位置 newElements[index] = element; // 将副本数组,赋值给CopyOnWriteArrayList的原数组 setArray(newElements); } finally { // 释放锁 lock.unlock(); } }

移除数据

关于remove操作,要分析两个方法

  • 基于索引位置移除指定数据
  • 基于具体元素删除数组中最靠前的数据
    • 当前这种方式,嵌套了一层,导致如果元素存在话,成本是比较高的。
    • 如果元素不存在,这种设计不需要加锁,提升写的效率
java
// 删除指定索引位置的数据 public E remove(int index) { // 拿锁,加锁 final ReentrantLock lock = this.lock; lock.lock(); try { // 获取原数组和原数组长度 Object[] elements = getArray(); int len = elements.length; // 通过get方法拿到index位置的数据 E oldValue = get(elements, index); // 声明numMoved int numMoved = len - index - 1; // 如果numMoved为0,说明删除的元素是最后的位置 if (numMoved == 0) // Arrays.copyOf复制一份新的副本数组,并且将最后一个数据不要了 // 基于setArray将副本数组赋值给array原数组 setArray(Arrays.copyOf(elements, len - 1)); else { // 删除的元素不在最后面的位置 // 声明副本数组,长度是原数组长度 - 1 Object[] newElements = new Object[len - 1]; // 从0开始复制的index前面 System.arraycopy(elements, 0, newElements, 0, index); // 从index后面复制到最后 System.arraycopy(elements, index + 1, newElements, index, numMoved); setArray(newElements); } // 返回被干掉的数据 return oldValue; } finally { // 释放锁 lock.unlock(); } } // 删除元素(最前面的) public boolean remove(Object o) { // 没加锁!!!! // 获取原数组 Object[] snapshot = getArray(); // 用indexOf获取元素在数组的哪个索引位置 // 没找到的话,返回-1 int index = indexOf(o, snapshot, 0, snapshot.length); // 如果index < 0,说明元素没找到,直接返回false,告辞 // 如果找到了元素的位置,直接执行remove方法的重载 return (index < 0) ? false : remove(o, snapshot, index); } // 执行remove(Object o),找到元素位置时,执行当前方法 private boolean remove(Object o, Object[] snapshot, int index) { // 拿锁,加锁 final ReentrantLock lock = this.lock; lock.lock(); try { // 拿到原数组和长度 Object[] current = getArray(); int len = current.length; // findIndex: 是给if起标识,break 标识的时候,直接跳出if的代码块~~ if (snapshot != current) findIndex: { // 如果没进到if,说明数组没变化,按照原来的index位置删除即可 // 进到这,说明数组有变化,之前的索引位置不一定对 // 拿到index位置和原数组长度的值 int prefix = Math.min(index, len); // 循环判断,数组变更后,是否影响到了要删除元素的位置 for (int i = 0; i < prefix; i++) { // 如果不相等,说明元素变化了。 // 同时判断变化的元素是否是我要删除的元素o if (current[i] != snapshot[i] && eq(o, current[i])) { // 如果满足条件,说明当前位置就是我要删除的元素 index = i; break findIndex; } } // 如果for循环结束,没有退出if,说明元素可能变化了,总之没找到要删除的元素 // 如果删除元素的位置,已经大于等于数组长度了。 if (index >= len) // 超过索引范围了,没法删除了。 return false; // 索引还在范围内,判断是否是还是原位置,如果是,直接跳出if代码块 if (current[index] == o) break findIndex; // 重新找元素在数组中的位置 index = indexOf(o, current, index, len); // 找到直接跳出if代码块 // 没找到。执行下面的return false if (index < 0) return false; } // 删除套路,构建新数组,复制index前的,复制index后的 Object[] newElements = new Object[len - 1]; System.arraycopy(current, 0, newElements, 0, index); System.arraycopy(current, index + 1, newElements, index, len - index - 1); // 复制到array setArray(newElements); // 返回true,删除成功 return true; } finally { lock.unlock(); } }

覆盖数据&清空集合

覆盖数据就是set方法,可以将指定位置的数据替换

java
// 覆盖数据 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); // 将element替换掉副本数组中的数据 newElements[index] = element; // 写回原数组 setArray(newElements); } else { // 原数据和新数据一样,啥不干,把拿出来的数组再写回去 setArray(elements); } // 返回原值 return oldValue; } finally { // 释放锁 lock.unlock(); } }

清空就是清空了~~~

java
public void clear() { // 加锁 final ReentrantLock lock = this.lock; lock.lock(); try { // 扔一个长度为0的数组 setArray(new Object[0]); } finally { lock.unlock(); } }

迭代器

用ArrayList时,如果想在遍历的过程中去移除或者修改元素,必须使用迭代器才可以。

即使写操作中原始数组被替换,迭代器仍访问旧数组,不让在迭代时做写操作是因为不希望迭代操作时,会影响到写操作,还有,不希望迭代时,还需要加锁。

java
// 获取遍历CopyOnWriteArrayList的iterator。 public Iterator<E> iterator() { // 其实就是new了一个COWIterator对象,并且获取了array,指定从0开始遍历 return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { /** 遍历的快照 */ private final Object[] snapshot; /** 游标,索引~~~ */ 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 E previous() { if (! hasPrevious()) 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(); } // 兼容函数式编程 @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] elements = snapshot; final int size = elements.length; for (int i = cursor; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } cursor = size; } }

总结

CopyOnWriteArrayList 是 Java 并发容器的独特存在,其优势在读操作完全无锁,适用于 读:写 > 100:1 的场景,由于是数组实现,在写操作上内存开销大,不适合频繁修改或大数据集,需要在享受 COW 的无锁读取红利时,务必警惕其写入代价。理解其底层复制机制,方能发挥最大威力!

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!