深入浅出CopyOnWriteArrayList

CopyOnWriteArrayList 是线程安全的,可以在多线程环境下使用。CopyOnWriteArrayList 遵循写时复制的原则,每当对列表进行修改(例如添加、删除或更改元素)时,都会创建列表的一个新副本,这个新副本会替换旧的列表,而对旧列表的所有读取操作仍然可以继续。由于在修改时创建了新的副本,所以读取操作不需要锁定。这使得在多读取者和少写入者的情况下读取操作非常高效。当然,由于每次写操作都会创建一个新的数组副本,所以会增加存储和时间的开销。如果写操作非常频繁,性能会受到影响。

什么是 CopyOnWrite

ReentrantReadWriteLock 读写锁是通过读写分离的思想来实现的,即读写锁将读写操作分别加锁,从而实现读写操作的并发执行。但是,读写锁也存在一些问题,比如说在写锁执行后,读线程会被阻塞,直到写锁被释放后读线程才有机会获取到锁从而读到最新的数据,站在读线程的角度来看,读线程在任何时候都能获取到最新的数据,满足数据实时性 。而 CopyOnWriteArrayList 是通过 Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略实现数据的最终一致性,并且能够保证读线程间不阻塞。当然,这要牺牲数据的实时性。通俗的讲,CopyOnWrite 就是当我们往一个容器添加元素的时候,不直接往容器中添加,而是先复制出一个新的容器,然后在新的容器里添加元素,添加完之后,再将原容器的引用指向新的容器。多个线程在读的时候,不需要加锁,因为当前容器不会添加任何元素。

CopyOnWriteArrayList原理

顾名思义,实际上 CopyOnWriteArrayList 内部维护的就是一个数组:

java 复制代码
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

该数组被 volatile 修饰,能够保证数据的内存可见性。

get 方法

get 方法的源码如下:

java 复制代码
public E get(int index) {
    return get(getArray(), index);
}
/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

get 方法的实现非常简单,几乎就是一个"单线程",没有添加任何的线程安全控制,没有加锁也没有 CAS 操作,原因就是所有的读线程只会读取容器中的数据,并不会进行修改。

add 方法

add 方法的源码如下:

java 复制代码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
	  //1. 使用Lock,保证写线程在同一时刻只有一个
    lock.lock();

    try {
				//2. 获取旧数组引用
        Object[] elements = getArray();
        int len = elements.length;

				//3. 创建新的数组,并将旧数组的数据复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);

				//4. 往新数组中添加新的数据
				newElements[len] = e;

				//5. 将旧数组引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

add 方法的逻辑也比较容易理解,需要注意这么几点:
1、采用 ReentrantLock 保证同一时刻只有一个写线程正在进行数组的复制;
2、通过调用 getArray() 方法获取旧的数组。

java 复制代码
final Object[] getArray() {
    return array;
}

03、然后创建一个新的数组,把旧的数组复制过来,然后在新的数组中添加数据,再将新的数组赋值给旧的数组引用。

java 复制代码
final void setArray(Object[] a) {
    array = a;
}

根据 volatile 的 happens-before 规则,所以这个更改对所有线程是立即可见的。
04、最后,在 finally 块中释放锁,以便其他线程可以访问和修改列表。

CopyOnWriteArrayList 的使用

CopyOnWriteArrayList 的使用非常简单,和 ArrayList 的使用几乎一样,只是在创建对象的时候需要使用 CopyOnWriteArrayList 的构造方法,如下所示:

java 复制代码
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("element1");
list.add("element2");

for (String element : list) {
    System.out.println(element);
}

CopyOnWriteArrayList 的缺点

CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要特别注意。
内存占用问题:因为 CopyOnWrite 的写时复制机制,在进行写操作的时候,内存里会同时有两个对象,旧的对象和新写入的对象。
数据一致性问题:CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器,最好通过 ReentrantReadWriteLock 自定义一个的列表。
比较一下 CopyOnWrite 和读写锁。
相同点:

  1. 两者都是通过读写分离的思想来实现的;
  2. 读线程间是互不阻塞的
    不同点:
    为了实现数据实时性,在写锁被获取后,读线程会阻塞;或者当读锁被获取后,写线程会阻塞,从而解决"脏读"的问题。而 CopyOnWrite 对数据的更新是写时复制的,因此读线程是延时感知的,单不会存在阻塞的情况。
相关推荐
腻害兔42 分钟前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
碎光拾影1 小时前
ARM交叉工具链各工具作用及IMX6ULL平台LED+蜂鸣器裸机程序实现
java·开发语言·数据库
Miao121312 小时前
微服务 API 测试实践:海外某民宿平台如何构建模式驱动测试基础设施
java·开发语言
腻害兔2 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:支付模块 yudao-module-pay,一个让产品经理都看懂的支付中台设计
java·前端·vue.js·产品经理·ai编程
CRMEB系统商城3 小时前
开源自建还是SaaS订阅?算一笔3年经济账
java·大数据·开发语言·开源
keyipatience4 小时前
线程栈与TLS和线程互斥
java·linux·服务器·开发语言·ubuntu
weixin_408099675 小时前
2026 图片去水印 API 接口完全指南:一键去除图片水印(附 Python/Java/PHP/C# 示例)
java·python·php·图片处理·api调用·图片去水印·石榴智能
钱六两5 小时前
#3、SpringAI 接入deepSeek大模型(喂饭版)
java·spring boot·ai编程
null_176 小时前
IntelliJ IDEA 极致流畅配置方案:Ultra 9 285K + 64GB 内存实测
java·ide·intellij-idea
Herbert_hwt6 小时前
建立Java程序开发
java·开发语言