news 2026/7/22 15:53:09

从源码看 CopyOnWriteArrayList 的线程安全机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从源码看 CopyOnWriteArrayList 的线程安全机制

目录

一.CpoyOnWriteArrayList 原理简单概述

二. CopyOnWriteArrayList 源码分析

2.1 属性构造器解读

2.2 get 方法分析

2.3 add 方法分析

2.4 set 方法分析

2.5 remove 方法分析

三. 总结概括


使用过 ArrayList 集合的同学应该大致都知道,ArrayList 是一个非线程安全的集合;

同样,Java也为我们提供了线程安全的 List 集合,它就是我们本篇文章要说的 CopyOnWriteArrayList。

只是因为开发过程中通常会使用其他的的方式保证线程安全,所以它使用的频率没有 ArrayList 那么频繁。

一.CpoyOnWriteArrayList 原理简单概述

CopyOnWriteArrayList 的底层采用了加锁的方式保证线程安全并且加的是 Lock 锁而不是 Sychonized 锁

假如现在有两个线程,一个读线程A,一个写线程B,同时想要想数组中添加元素,读线A程就会读取当前内存中 CopyOnWriteArrayList 集合,写线程B则是会将内存中的 CopyOnWriteArrayList 集合对象复制一份新的,在新复制的集合中执行添加操作,添加操作完成之后再将新的集合赋值给原来老的集合,并且这个过程中写线程B会获取唯一的 Lock 锁,其它写线程会阻塞等待,实现读写分离。那么假如说有第三个写线程C也想要执行写数据操作,就需要等待写线程B操作完成之后释放 Lock 锁自己获取到 Lock 锁之后才能去执行写入操作。

二. CopyOnWriteArrayList 源码分析

2.1 属性构造器解读

下面是我粘贴的一部分属性,get,set方法,构造方法。

(1)可以看到在 CopyOnWriteArrayList 内部它定义了一个 Lock 锁对象;

(2)底层定义了一个名为 array 的对象数组;

(3)无参构造可以看出调用无参构造会将 array 数组对象的长度设置为 0,只有在进行存储元素的时候才回去扩容;

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array; /** * Gets the array. Non-private so as to also be accessible * from CopyOnWriteArraySet class. */ final Object[] getArray() { return array; } /** * Sets the array. */ final void setArray(Object[] a) { array = a; } /** * Creates an empty list. */ public CopyOnWriteArrayList() { setArray(new Object[0]); }

2.2 get 方法分析

下面是 CopyOnWriteArrayList 的 get 获取元素的方法,这里它 index 和数组的长度大小都没有做判断,所以很有可能会出现索引越界异常;

get 获取元素的方法很简单,没有出现加锁的行为

public E get(int index) { // 直接返回对象 index 位置的元素 return get(getArray(), index); }

2.3 add 方法分析

public void add(int index, E element) { // 获取 Lock 锁 final ReentrantLock lock = this.lock; // 调用方法上锁 lock.lock(); try { // 获取内存的数组对象并赋值为 elements Object[] elements = getArray(); // 定义一个变量 len 获取数组的长度 int len = elements.length; // 判断方法的参数 index 是否越界或合法 if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); // 定义一个新数组对象 newElements Object[] newElements; // 定义一个变量 numMoved 接收数组长度 - index 的值 int numMoved = len - index; // 如果 numMoved 为0,则说明要把新添加的元素放在数组的最后 if (numMoved == 0) // 调用调用 copyOf 方法将原来数组中的数据全部复制到 newElements中, // 并在数组末尾添加上新的元素 newElements = Arrays.copyOf(elements, len + 1); else { // 如果 numMoved 不为0,则说明要将该元素添加在数组中间的某个位置 // 先将新数组的长度 + 1 newElements = new Object[len + 1]; // 将老数组 0~index 之间的数据全部复制到新数组中 System.arraycopy(elements, 0, newElements, 0, index); // 再将 index~数组最后的数据全部复制到新数组中 System.arraycopy(elements, index, newElements, index + 1, numMoved); } // 将要添加的元素 element 添加到新数组的 index 的位置 newElements[index] = element; // 将老数组地址值赋值给新数组对象 setArray(newElements); } finally { // 操作完毕,最后释放锁 lock.unlock(); } }

2.4 set 方法分析

public E set(int index, E element) { // 获取 Lock 锁 final ReentrantLock lock = this.lock; // 调用方法上锁 lock.lock(); try { // 获取内存的数组对象并赋值给一个新的数组对象 elements Object[] elements = getArray(); // 获取 index 处的元素 E oldValue = get(elements, index); // 判断 oldValue 和要插入的元素是否相等 if (oldValue != element) { // 获取数组的长度 int len = elements.length; // 将原本的数组数据复制到新数组 newElements 中 Object[] newElements = Arrays.copyOf(elements, len); // 将 element 放置到新数组的 index 处 newElements[index] = element; // 将新数组覆盖原来的数组 setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics // 进入 else ,说明要set的元素在数组中已经存在,直接返回原数组 setArray(elements); } // 返回位置 index 处的老的元素 return oldValue; } finally { // 操作完成,释放 lock 锁 lock.unlock(); } }

2.5 remove 方法分析

public E remove(int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); // 获取数组长度 int len = elements.length; // 获取 index 处的元素 E oldValue = get(elements, index); // 定义 numMoved 计算出要移动的元素的数量 int numMoved = len - index - 1; // 如果 numMoved 为0,说明要删除的元素恰好是数组的最后一个元素 if (numMoved == 0) // 覆盖原来的数组 setArray(Arrays.copyOf(elements, len - 1)); else // numMoved 不为0,则定义一个新数组,长度为原来的数组长度-1 Object[] newElements = new Object[len - 1]; // 将 0~index 处的元素复制到新数组中去 System.arraycopy(elements, 0, newElements, 0, index); // 将 index+1~数组最后的元素移动到新数组中 System.arraycopy(elements, index + 1, newElements, index, numMoved); // 覆盖原来的数组 setArray(newElements); } // 返回删除的元素值 return oldValue; } finally { // 操作完毕,释放 lock 锁 lock.unlock(); } }

三. 总结概括

经过上面对 add 添加方法,get 获取方法,set 修改方法,remove 删除方法的分析,其实也可以看出,相比于原来 ArrayList 集合的基础上添加了一把 lock 。

在做增,改,删三种操作的时候,搭配上 copy 复制数组的思想,就可以做到线程安全,这就是 CopyOnWriteArrayList 线程安全的核心设计思想,不算特别难理解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 15:53:01

浅谈 MySQL 主从复制,优点?原理?

目录 一. 主从复制概述 二. 主从复制有什么优点&#xff1f; 三. 主从复制的原理 四. 数据一致性问题 4.1 同步复制 4.2 异步复制 4.3 半同步复制 一. 主从复制概述 既然是主从复制&#xff0c;那么至少就应该有两台服务器&#xff0c;一台作为主库(Master)&#xff0c…

作者头像 李华
网站建设 2026/7/22 15:51:48

虚拟线程如何用“同步代码”血洗百万并发?

虚拟线程如何用“同步代码”血洗百万并发&#xff1f; 文章目录虚拟线程如何用“同步代码”血洗百万并发&#xff1f;一、传统并发模型的痛点与瓶颈二、直击灵魂&#xff1a;虚拟线程到底是个啥&#xff1f;三、硬核对决&#xff1a;传统线程池 vs 虚拟线程传统线程池方式&…

作者头像 李华
网站建设 2026/7/22 15:50:34

TM4C129X以太网PHY寄存器深度解析:从基础配置到高级调试实践

1. 以太网PHY配置寄存器&#xff1a;从基础到TM4C129X的深度实践搞嵌入式网络开发&#xff0c;尤其是用到以太网接口&#xff0c;PHY芯片的配置绝对是个绕不开的坎。很多工程师习惯直接用厂商的驱动库&#xff0c;初始化函数一调&#xff0c;能ping通就万事大吉。但一旦遇到网络…

作者头像 李华
网站建设 2026/7/22 15:49:18

MyBatis核心流程以及工作原理

MyBatis核心对象 根据以下这四大核心对象&#xff0c;我们就能理清MyBatis的工作原理。 SqlSession对象&#xff0c;该对象中包含了执行SQL语句的所有方法。类似于JDBC里面的Connection。 Executor接口&#xff0c;它将根据SqlSession传递的参数动态地生成需要执行的SQL语句&…

作者头像 李华
网站建设 2026/7/22 15:45:06

金属加工核心人才留不住?北京华恒智信薪酬优化成功案例

【客户行业】生产制造行业【问题类型】薪酬改革【客户背景及现状】某大型金属加工公司隶属于大型央企集团&#xff0c;至今成立已有60余年。公司主导业务是进行某有色金属的生产加工&#xff0c;其产品广泛应用于电子通讯、轨道交通、航空航天、国防军工、船舶制造等高端领域&a…

作者头像 李华