CopyOnWriteArrayList 的实现原理和适用场景(源码)

devtools/2025/3/13 13:51:43/

CopyOnWriteArrayList的实现原理

目录

  • CopyOnWriteArrayList的实现原理
    • 核心源码解读
      • (1)数据结构:采用数组
      • (2)初始化
      • (3)add 操作
      • (4)get操作
      • (5)remove操作
      • (6)计数size
      • (7)遍历操作
    • 使用场景

  1. 读操作无锁:直接访问 volatile 数组,弱一致性(可能读到旧数据)。
  2. 写操作加锁:通过 ReentrantLock 保证原子性,每次修改复制新数组。
  3. 空间换时间:写操作频繁时性能较差,适合读多写少的场景。

核心源码解读

(1)数据结构:采用数组

volatile 保证内存可见性

java">private transient volatile Object[] array;

(2)初始化

java">// 默认创建空数组
public CopyOnWriteArrayList() {setArray(new Object[0]);
}// 可以传入集合进行初始化
public CopyOnWriteArrayList(Collection<? extends E> c) {Object[] elements;if (c.getClass() == CopyOnWriteArrayList.class)elements = ((CopyOnWriteArrayList<?>)c).getArray();else {elements = c.toArray();if (c.getClass() != ArrayList.class)elements = Arrays.copyOf(elements, elements.length, Object[].class);}setArray(elements);
}// 可以传入数组进行初始化
public CopyOnWriteArrayList(E[] toCopyIn) {setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

数组复制使用 Arrays.copyOf

(3)add 操作

加锁确保 写操作的原子性。

java">final transient ReentrantLock lock = new ReentrantLock();

复制原数组,追加新元素

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

(4)get操作

直接访问数组下标,无锁操作

java">public E get(int index) {return get(getArray(), index);
}private E get(Object[] a, int index) {return (E) a[index];
}final Object[] getArray() {return array;
}

(5)remove操作

主要流程:

  1. 先根据当前数组快照,查询是否包含目标元素,若不包含直接返回false;
  2. 找到目标元素,加锁,再次获取当前最新数组,遍历快照与当前数组的公共前缀 prefix ,若有更早的匹配项,更新要删除的下标索引index ;
  3. 否则判断index是否大于新的长度len,若是,返回false;
  4. 否则判断若当前数组在 index 处仍匹配元素,无需更新索引;
  5. 否则重新搜索当前数组中该元素的位置index,若不存在, 返回false ;
  6. 跳过要删除的元素位置index,复制新数组;
java">public boolean remove(Object o) {// 获取当前数组快照(volatile读,保证可见性)Object[] snapshot = getArray();int index = indexOf(o, snapshot, 0, snapshot.length);return (index < 0) ? false : remove(o, snapshot, index);
}private boolean remove(Object o, Object[] snapshot, int index) {final ReentrantLock lock = this.lock;lock.lock();try {// 再次获取当前最新数组Object[] current = getArray();int len = current.length;// 检查快照 snapshot 是否过时(其他线程写操作可能已修改数组)if (snapshot != current) findIndex: {// 遍历快照与当前数组的公共前缀 prefix,避免全数组扫描,提升性能int prefix = Math.min(index, len);for (int i = 0; i < prefix; i++) {// 若发现当前数组中有对位不等的更早的匹配项(删除或替换了原index前的元素),更新下标index,退出循环if (current[i] != snapshot[i] && eq(o, current[i])) {index = i;break findIndex;}}// 若原下标index已超出当前数组长度,直接返回失败if (index >= len)return false;// 若当前数组在 index 处仍匹配元素,无需更新索引if (current[index] == o)break findIndex;// 否则重新搜索当前数组中该元素的位置index = indexOf(o, current, index, len);if (index < 0)return false;}Object[] newElements = new Object[len - 1];// 复制 index 前的元素System.arraycopy(current, 0, newElements, 0, index);// 复制 index 后的元素(跳过要删除的元素)System.arraycopy(current, index + 1,newElements, index,len - index - 1);// 更新数组引用(volatile写,保证可见性)setArray(newElements);return true;} finally {lock.unlock();}
}

根据下标删除,直接加锁,跳过要删除的下标,复制新数组。

java">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);// 确认要跳过的下标 indexint 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();}
}

(6)计数size

直接访问数组长度

java">public int size() {return getArray().length;
}

(7)遍历操作

迭代器基于 快照数组 实现,确保遍历过程中数据的一致性,但无法反映遍历期间其他线程对列表的修改。

迭代器无法直接修改原数据,直接抛出异常。

java">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; // 快照数组private int cursor; // 遍历下标位置// 构造函数:绑定快照数组 和 初始位置 0 private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;}// 直接比较下标和快照数组长度public boolean hasNext() {return cursor < snapshot.length;}// 返回当前元素后下标自增@SuppressWarnings("unchecked")public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}public boolean hasPrevious() {return cursor > 0;}@SuppressWarnings("unchecked")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();}@Overridepublic 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 中一种特殊的线程安全集合,核心设计思想是 写时复制Copy-On-Write)和 快照数组。

(1)适用于读多写少的并发环境,数据频繁读取,但写入操作极少。由于每次写入都会复制整个数组,内存和性能开销大,因此不适用于需要频繁写入的场景。

(2)遍历过程中需要保证数据的一致性(即使数据被其他线程修改)。


http://www.ppmy.cn/devtools/166785.html

相关文章

Pygame实现射击鸭子游戏3-3

6 游戏配置设置 游戏配置设置的代码如图10所示。 图10 游戏配置设置的代码 其中&#xff0c;第32行代码初始化pygame&#xff1b;第33-34行代码设置了屏幕的宽度和高度&#xff1b;第35行代码设置了鸭子的数量&#xff1b;第36行代码创建屏幕&#xff1b;第37行代码设置屏幕的…

SQL-留存率

一、留存率业务含义 留存率可以评用户对产品的粘性&#xff0c;留存率越低用户对产品的粘性越小 留存率通常分为次日留存率、3日留存率、7日留存率、30日留存率 这里以新增用户留存率为例&#xff1a; 次日留存率&#xff1a;(基准日之后的第1天留存的用户数)/基准日当天新…

C++和标准库速成(一)——HelloWorld和名称空间

目录 1. 引言1. 简单小程序"Hello World"1.1 模块导入1.2 预处理指令1.2.1 简介1.2.2 常用的预处理指令 1.3 main()函数1.4 输入输出流1.4.1 输出流1.4.2 转义字符1.4.3 输入流 2. 名称空间2.1 定义名称空间2.2 using指令2.3 嵌套名称空间2.4 名称空间别名 参考 1. 引…

【抽奖项目】|第三篇

前言&#xff1a; 高并发的活动预热肯定不可以在数据库操作&#xff0c;需要redis&#xff0c;特别是这种秒杀活动更是需要注意&#xff0c;所以可以在高并发的前夕先进行活动预热。 上一篇写完了怎么样活动预热&#xff0c;可以看看这篇写怎么样高并发抽奖接口 【抽奖项目】|第…

pytorch心德

以下是一些关于写Pytorch项目心得的思路呀&#xff1a; 项目整体流程方面 环境搭建与数据准备 描述在安装Pytorch及相关依赖库时遇到的问题及解决办法&#xff0c;比如CUDA版本与Pytorch版本的匹配问题等。 谈一谈数据收集、清洗、标注等过程的难度和重要性&#xff0c;以及在处…

JAVA实现好看的俄罗斯方块小游戏(附源码)

文章目录 一、设计来源俄罗斯方块小游戏讲解1.1 主界面1.2 游戏中界面1.3 游戏结束界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载更多优质源码分享 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/146156297 JAVA…

Javascript 2016-2024 新特性讲解

JavaScript 作为世界上最流行的编程语言之一&#xff0c;一直在不断发展和进化。从 2016 年的 ES7 到 2024 年的 ES15&#xff0c;每年都会有新的语言特性和功能被添加进来。这些更新不仅提升了语言的表达能力&#xff0c;也为开发者提供了更多便利的工具和更优雅的解决方案。 …

算法题(94):除2!

审题&#xff1a; 本题需要我们对n个数据中的偶数数据进行不大于k次除2操作&#xff0c;使得n个数据的总和最小 思路&#xff1a; 方法一&#xff1a;贪心与优先级队列&#xff08;大堆&#xff09; 贪心策略&#xff1a;我们每次都对目前最大的偶数进行除2的操作 策略证明&…