Java并发基础:CopyOnWriteArrayList全面解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: CopyOnWriteArrayList类的最大优点在于读取时无需加锁,非常适合读多写少的并发场景,由于其写操作通过复制底层数据来实现,从而保证了读取数据的一致性和高效性,此外,它简单易用,是快速实现线程安全列表的不错选择,CopyOnWriteArrayList在读操作占主导的场景下,能够提供出色的性能和稳定性。

Java并发基础:CopyOnWriteArrayList全面解析 - 程序员古德

内容概要

CopyOnWriteArrayList类的最大优点在于读取时无需加锁,非常适合读多写少的并发场景,由于其写操作通过复制底层数据来实现,从而保证了读取数据的一致性和高效性,此外,它简单易用,是快速实现线程安全列表的不错选择,CopyOnWriteArrayList在读操作占主导的场景下,能够提供出色的性能和稳定性。

核心概念

CopyOnWriteArrayList 类实现了 ListRandomAccessCloneable接口,它是一个线程安全的变体,它的工作原理:当修改操作(如 addset 等)发生时,它会复制底层数组,然后在复制后的数组上进行修改,修改完成后再将内部的引用指向新的数组,这种设计使得读取操作可以在不进行任何锁定的情况下进行,因此非常适合读多写少的并发场景。

假设,有一个在线新闻发布系统,该系统维护了一个新闻列表,这个列表需要被频繁地读取(用户浏览新闻),但只偶尔被修改(发布新新闻或更新现有新闻),在这种情况下,使用 CopyOnWriteArrayList 来存储新闻列表可能是一个不错的选择。

  1. 读取操作:当用户浏览新闻时,系统需要从新闻列表中读取数据,由于 CopyOnWriteArrayList 的读取操作是无锁的,因此多个用户可以同时读取新闻列表而不会相互阻塞,这有助于提高系统的吞吐量。
  2. 写入操作:当新闻编辑发布新新闻或更新现有新闻时,系统需要修改新闻列表,虽然 CopyOnWriteArrayList 的写入操作会复制整个底层数组,但由于这种操作并不频繁,因此不会成为性能瓶颈,此外,由于写入操作是在新的数组上进行的,因此读取操作不会被阻塞,这有助于保持系统的响应性。

CopyOnWriteArrayList 类特别适合读多写少的场景中,它通常用来解决以下类似场景的问题:

  1. 线程安全:在多线程环境下,普通的 ArrayList 不是线程安全的,如果有多个线程同时修改 ArrayList,可能会导致数据不一致的问题,CopyOnWriteArrayList 通过内部的复制机制保证了线程安全,即当有线程对列表进行修改时(如添加、删除元素),它会先复制一份当前列表的副本,然后在副本上进行修改,修改完成后再将内部引用指向新的副本,这样读取操作就可以继续在原列表上进行,而不会被修改操作所阻塞。
  2. 读写分离:由于 CopyOnWriteArrayList 的写操作(修改操作)是在新的数组上进行的,而读操作则是在未修改的原始数组上进行,因此读写操作之间不会相互干扰,这种读写分离的设计使得 CopyOnWriteArrayList 在高并发读取的场景下表现良好。
  3. 数据一致性:通过复制整个底层数组来确保修改操作的原子性,CopyOnWriteArrayList 提供了强一致性保证,因此,读取操作要么看到的是修改之前的列表状态,要么看到的是修改之后的列表状态,而不会是中间状态。

    CopyOnWriteArrayList 并不适用于所有场景,由于其写操作需要复制整个底层数组,因此当列表很大或者写操作非常频繁时,它可能会导致较大的内存开销和性能下降。

代码案例

下面是一个简单的Java程序,演示了如何使用CopyOnWriteArrayList类,案例中创建了一个CopyOnWriteArrayList实例,并模拟了多个线程同时对其进行读写操作的情况,如下代码:

import java.util.List;  
import java.util.concurrent.CopyOnWriteArrayList;  

public class CopyOnWriteArrayListDemo {
   
     

    public static void main(String[] args) {
   
     
        // 创建一个CopyOnWriteArrayList实例  
        List<String> list = new CopyOnWriteArrayList<>();  

        // 启动一个线程向列表中添加元素  
        new Thread(() -> {
   
     
            for (int i = 0; i < 5; i++) {
   
     
                list.add("Item " + i);  
                try {
   
     
                    // 模拟耗时操作,让其他线程有机会读取数据  
                    Thread.sleep(100);  
                } catch (InterruptedException e) {
   
     
                    e.printStackTrace();  
                }  
            }  
            System.out.println("Writer Thread Finished!");  
        }).start();  

        // 启动多个线程从列表中读取元素  
        for (int i = 0; i < 3; i++) {
   
     
            new Thread(() -> {
   
     
                for (int j = 0; j < 10; j++) {
   
     
                    // 读取列表中的元素  
                    System.out.println("Reader Thread " + Thread.currentThread().getId() + " is reading: " + list);  
                    try {
   
     
                        // 模拟耗时操作  
                        Thread.sleep(50);  
                    } catch (InterruptedException e) {
   
     
                        e.printStackTrace();  
                    }  
                }  
            }).start();  
        }  
    }  
}

在上面代码中,创建了一个CopyOnWriteArrayList的实例list,启动了一个线程来模拟向列表中添加元素的写操作,这个线程使用循环向列表中添加5个元素,并在每次添加后暂停100毫秒以模拟耗时操作,接着,启动了3个线程来模拟从列表中读取元素的读操作,每个线程都使用循环读取列表10次,并在每次读取后暂停50毫秒以模拟耗时操作,由于CopyOnWriteArrayList的写操作会复制底层数组,并且在新的数组上进行修改,因此读取操作不会受到写操作的影响,从而保证了线程安全和数据一致性。

核心API

CopyOnWriteArrayList 类提供了一个线程安全的 ArrayList 实现,它通过在修改时复制底层数组来实现线程安全,因此它特别适合于读多写少的并发场景,下面是CopyOnWriteArrayList` 类中一些主要方法的含义:

1、构造方法

  • CopyOnWriteArrayList(): 创建一个空的 CopyOnWriteArrayList
  • CopyOnWriteArrayList(Collection<? extends E> c): 创建一个包含指定集合中所有元素的 CopyOnWriteArrayList

2、读取方法(通常不会阻塞,因为读取时不需要锁定)

  • get(int index): 获取指定索引位置的元素。
  • iterator(): 返回一个迭代器,用于遍历列表中的元素。
  • listIterator(): 返回一个列表迭代器,允许以任何方向遍历列表,并修改元素(尽管在 CopyOnWriteArrayList 中修改元素实际上会抛出 UnsupportedOperationException)。
  • toArray(): 返回一个包含列表中所有元素的数组。

3、修改方法(在修改时因为会复制底层数组,所以可能需要更多时间)

  • add(E e): 在列表的末尾添加一个元素。
  • add(int index, E element): 在列表的指定位置插入一个元素。
  • addAll(Collection<? extends E> c): 将指定集合中的所有元素添加到列表的末尾。
  • addAll(int index, Collection<? extends E> c): 将指定集合中的所有元素插入到列表的指定位置。
  • set(int index, E element): 替换列表中指定位置的元素。
  • remove(int index): 移除列表中指定位置的元素。
  • remove(Object o): 移除列表中第一个出现的指定元素(如果存在)。
  • removeAll(Collection<?> c): 移除列表中所有包含在指定集合中的元素。
  • retainAll(Collection<?> c): 仅在列表中保留包含在指定集合中的元素。
  • clear(): 清空列表中的所有元素。

4、查询方法

  • contains(Object o): 检查列表中是否包含指定元素。
  • containsAll(Collection<?> c): 检查列表是否包含指定集合中的所有元素。
  • isEmpty(): 检查列表是否为空。
  • size(): 返回列表中的元素数量。
  • indexOf(Object o): 返回列表中第一次出现指定元素的索引,如果列表不包含此元素,则返回 -1。
  • lastIndexOf(Object o): 返回列表中最后一次出现指定元素的索引,如果列表不包含此元素,则返回 -1。

5、其他方法

  • subList(int fromIndex, int toIndex): 返回列表中指定的部分视图(注意,返回的不是新的独立列表,对返回的列表的修改会影响原列表,但由于 CopyOnWriteArrayList 的特性,修改操作会抛出 UnsupportedOperationException)。
  • equals(Object o): 比较此列表与指定对象是否相等。
  • hashCode(): 返回列表的哈希码值。

    CopyOnWriteArrayList 的写操作(修改方法)是通过复制底层数组来实现的,因此在高并发写入的场景下,性能可能会受到严重影响,这种情况下,其他并发数据结构,如 ConcurrentHashMap 中的 Segment 数组、ConcurrentLinkedQueueConcurrentSkipListMap 等,可能会是更好的选择。但是,在读多写少的场景中,推荐使用CopyOnWriteArrayList ,它提供了一个高效且线程安全的解决方案。

核心总结

Java并发基础:CopyOnWriteArrayList全面解析 - 程序员古德

CopyOnWriteArrayList类是一个线程安全的列表实现,非常适合读多写少的并发场景,优点在于读取操作无需锁定,性能高效,且能保证数据的一致性;写操作时,通过复制底层数组来避免阻塞读操作,从而实现读写分离。但是,它的写操作代价较高,因为每次修改都需要复制整个数组,这在数据量大或写操作频繁时会造成明显的性能开销,此外,它不适合需要频繁修改列表的场景,因为每次修改都会生成新的数组副本,导致内存占用较高。

在读取操作远多于写入操作,且对数据实时性要求不高的场景中,CopyOnWriteArrayList是个不错的选择,但如果写操作频繁或数据量巨大,建议考虑其他更适合的并发数据结构,如ConcurrentLinkedQueueConcurrentHashMap等。

关注我,每天学习互联网编程技术 - 程序员古德

END!
END!
END!

往期回顾

精品文章

Java并发基础:ConcurrentSkipListMap全面解析

Java并发基础:ConcurrentSkipListSet全面解析!

Java并发基础:SynchronousQueue全面解析!

Java并发基础:ConcurrentLinkedQueue全面解析!

Java并发基础:Exchanger全面解析!

精彩视频

相关文章
|
4天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
24天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
24天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
27天前
|
Java 数据库连接 开发者
Java中的异常处理机制:深入解析与最佳实践####
本文旨在为Java开发者提供一份关于异常处理机制的全面指南,从基础概念到高级技巧,涵盖try-catch结构、自定义异常、异常链分析以及最佳实践策略。不同于传统的摘要概述,本文将以一个实际项目案例为线索,逐步揭示如何高效地管理运行时错误,提升代码的健壮性和可维护性。通过对比常见误区与优化方案,读者将获得编写更加健壮Java应用程序的实用知识。 --- ####
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
77 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
81 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
67 0
|
3天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
3天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析

推荐镜像

更多