当前位置: 代码迷 >> 综合 >> 【集合List系列四】CopyOnWriteArrayList
  详细解决方案

【集合List系列四】CopyOnWriteArrayList

热度:53   发布时间:2023-12-16 02:55:43.0

???欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
img

  • 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
  • 导航
    • 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
    • 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
    • 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
    • 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
    • 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。??? ?? 欢迎订阅本专栏 ??

博客目录

      • 1.什么是 CopyOnWriteArrayList
      • 2.CopyOnWriteArrayList 特点
      • 3.CopyOnWriteArrayList 使用场景
      • 4.CopyOnWriteArrayList 简介
      • 5.CopyOnWriteArrayList 原理
      • 6.CopyOnWriteArrayList 的 add 方法
      • 7.写时复制的缺点?

1.什么是 CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 中的一个线程安全的 List 实现类,它是 ArrayList 的线程安全版本。CopyOnWriteArrayList 允许多个线程同时读取 List 中的元素,而不需要进行额外的同步操作。当有写操作时,CopyOnWriteArrayList 会将 List 中的元素复制一份,然后进行修改,修改完成后再将新的 List 替换原来的 List。

2.CopyOnWriteArrayList 特点

CopyOnWriteArrayList 的主要特点如下:

  1. 线程安全:CopyOnWriteArrayList 是线程安全的,可以在多线程环境中安全地使用。
  2. 读操作不锁定:CopyOnWriteArrayList 的读操作不需要进行锁定,因此读操作的性能很高。
  3. 写操作复制数组:CopyOnWriteArrayList 的写操作会将 List 中的元素复制一份,然后进行修改,修改完成后再将新的 List 替换原来的 List。因此,写操作的性能较低,而且会消耗更多的内存。

特点:

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
  • 写操作需要加锁,防止并发写入时导致写入数据丢失。
  • 写操作结束之后需要把原始数组指向新的复制数组。
  • 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右。
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
  • 写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。

3.CopyOnWriteArrayList 使用场景

CopyOnWriteArrayList 的使用场景主要是在读操作远远多于写操作的情况下,例如缓存和事件监听器列表等。

CopyOnWriteArrayList 为 ArrayList 的线程安全版本,俗称写时复制,此思想在多种中间件中都有使用,比如 nacos,sentinel 等中间件.空间换时间,保证读的最大效率,牺牲一点写的性能.多使用在都多写少的场景下.由于读操作根本不会修改原有的数据,因此如果每次读取都进行加锁操作,其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读操作是线程安全的。

CopyOnWriteArrayList 其底层数据结构也是数组,但是在写操作的时候都会拷贝一份数据进行修改,修改完后替换掉老数据,从而保证只阻塞写操作,读操作不会阻塞,实现读写分离。

4.CopyOnWriteArrayList 简介

  • CopyOnWriteArrayList 线程安全,默认容量为长度为 1 的 Object 数组,允许元素为 null。
  • 使用 ReentrantLock 可重入锁,保证写操作的线程安全。
  • 在写操作时,都需要拷贝一份数组,然后在拷贝的数组中进行相应的操作,最后再替换旧数组。
  • 采用读写分离的实现,写操作加锁,读操作不加锁,而且写操作会占用较多空间,因此适用于读多写少的场景。
  • CopyOnWriteArrayList 能保证最终一致性,但是不保证实时一致性,因为在写操作未完,而进行读操作时,由于写操作在新数组中操作,并不会影响到读操作,这是造成数据不一致性。
  • CopyOnWriteArrayList 返回迭代器不会抛出 ConcurrentModificationException 异常,即它不是 fail-fast 机制的

5.CopyOnWriteArrayList 原理

  • CopyOnWriteArrayList 默认容量是数组长度为 1 的 Object 类型数组。

  • 操作 array 底层数组,都是通过 setArray 和 getArray 来进行的。

/** The lock protecting all mutators */
// 使用可重入锁进行加锁,保证线程安全
final transient ReentrantLock lock = new ReentrantLock();/** The array, accessed only via getArray/setArray. */
// 底层数据结构,注意这里用volatile修饰,确定了多线程情况下的可见性
private transient volatile Object[] array;

CopyOnWriteArrayList原理:

  1. CopyOnWriteArrayList 实现了 List 接口,因此它是一个队列。
  2. CopyOnWriteArrayList 包含了成员 lock。每一个 CopyOnWriteArrayList 都和一个监视器锁 lock 绑定,通过 lock,实现了对 CopyOnWriteArrayList 的互斥访问。
  3. CopyOnWriteArrayList 包含了成员 array 数组,这说明 CopyOnWriteArrayList 本质上通过数组实现的。
  4. CopyOnWriteArrayList 的“动态数组”机制 – 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”。这就是它叫做 CopyOnWriteArrayList 的原因!CopyOnWriteArrayList 就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很 低;但是单单只是进行遍历查找的话,效率比较高。
  5. CopyOnWriteArrayList 的“线程安全”机制 – 是通过 volatile 和 ReentrantLock 来实现的。
  6. CopyOnWriteArrayList 是通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的 保证。
  7. CopyOnWriteArrayList 通过监视器锁 ReentrantLock 来保护数据。在“添加/修改/删除”数据时,会先“获取监视器锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。

6.CopyOnWriteArrayList 的 add 方法

add 操作是加了锁的,利用了 ReentrantLock 进行加锁,注意使用该方式进行加锁,需要手动释放。

整个过程是新建了一个新的数组(数组长度加 1),然后将新元素放在最后一位,最后替换掉旧数组

public boolean add(E e) {
    final ReentrantLock lock = this.lock;lock.lock();try {
    Object[] elements = getArray();int len = elements.length;// 注意这里将数组长度加1Object[] newElements = Arrays.copyOf(elements, len + 1);// 新元素放在最后一位newElements[len] = e;setArray(newElements);return true;} finally {
    lock.unlock();}
}

7.写时复制的缺点?

内存占用问题:

  • 因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,这一点会占用额外的内存空间。
  • 在元素较多或者复杂的情况下,复制的开销很大
  • 复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能。

数据一致性问题:由于 CopyOnWrite 容器的修改是先修改副本,所以这次修改对于其他线程来说,并不是实时能看到的,只有在修改完之后才能体现出来。如果你希望写入的的数据马上能被其他线程看到,CopyOnWrite 容器是不适用的。volatile 关键字,数据可见性

private transient volatile Object[] array;

觉得有用的话点个赞 ?? 呗。
??????本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!???

???如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!? ? ?

???Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!???

img

  相关解决方案