基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

CopyOnWriteArrayList:写时复制与并发机制

知识点图片

CopyOnWriteArrayList是一个线程安全的List,采用“写时复制”实现。它牺牲写性能(每次修改都复制数组)换取极高的读性能(无锁),因此最适用于“读多写少”的并发场景。

我们来详细地讲解一下 Java 中的 CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 并发包 java.util.concurrent 下一个非常重要的线程安全的 List 实现。它的名字“Copy-On-Write”(写时复制)已经完美地概括了其核心工作原理。


1. 什么是 Copy-On-Write (写时复制)?

这是一种用于并发编程的优化策略。其核心思想是:

  • 读操作:当进行读操作时(如 get(), iterator()),不加锁,直接读取底层数组的内容。因为读操作不修改数据,所以是线程安全的。
  • 写操作:当需要对列表进行修改时(如 add(), set(), remove()),它不直接在当前数组上修改,而是:
    1. 加锁:锁定整个列表,确保同一时间只有一个线程在进行写操作。
    2. 复制:创建一个底层数组的全新副本。
    3. 修改:在新的副本上进行添加、修改或删除操作。
    4. 替换:将指向旧数组的引用,切换到指向这个修改后的新数组。
    5. 解锁

整个过程就像你编辑一个共享文档时,不直接在原文件上改,而是先“另存为”一个副本,在副本上修改,修改完成后再用副本替换掉原文件。


2. CopyOnWriteArrayList 的主要特点

根据其“写时复制”的原理,我们可以推导出它的几个关键特性:

1. 线程安全 (Thread-Safe)

它通过在写操作时加锁和创建副本来保证线程安全,你可以在多线程环境中直接使用它,无需额外的同步措施。

2. 读操作性能极高

读操作完全不加锁,直接访问内存中的数组,效率非常高。这使得 CopyOnWriteArrayList 特别适合“读多写少”的场景。

3. 写操作性能较低

每次写操作都需要复制整个底层数组,这是一个非常耗时的操作。如果列表很大,或者写操作非常频繁,性能会急剧下降。

4. 数据一致性是“最终一致性”

这是一个非常重要的特性。当一个线程正在修改列表时(即在新的副本上操作),其他线程的读操作仍然在访问旧的、未修改的数组。只有当写操作完成,引用被切换后,后续的读操作才能访问到新的数据。

这意味着读操作可能读到的是“快照”数据(旧数据)

5. 迭代器是弱一致性的 (Weakly Consistent)

  • 当你从 CopyOnWriteArrayList 获取一个迭代器(iterator)时,这个迭代器引用的是创建它那个时刻的底层数组快照。
  • 之后任何对列表的修改(增、删、改)都与这个迭代器无关。
  • 因此,它的迭代器永远不会抛出 ConcurrentModificationException

3. 适用场景 (Use Cases)

CopyOnWriteArrayList 的设计初衷非常明确,它适用于以下场景:

  • 读多写少的并发场景:这是最典型的应用场景。例如:
    • 事件监听器 (Listeners/Observers):一个组件的监听器列表,通常在初始化时添加几个,之后很少改变,但在事件发生时会被频繁地遍历和调用。
    • 配置信息:系统加载时读取配置,运行时很少修改,但会被各个模块频繁读取。
    • 黑白名单:一些需要频繁校验的名单,但名单本身不常变动。

4. 不适用场景 (When to Avoid)

  • 写操作频繁的场景:如果你的应用需要频繁地修改列表,使用它会导致大量的数组复制和内存开销,性能会非常差。
  • 要求数据实时性的场景:由于读操作读取的是快照,如果你要求一个线程的写操作必须立刻对其他线程可见,CopyOnWriteArrayList 可能不适合,因为它存在短暂的数据不一致。
  • 列表数据量非常大:列表越大,单次写操作的复制成本就越高。

5. 与其他线程安全 List 的对比

特性 / List CopyOnWriteArrayList Vector Collections.synchronizedList(new ArrayList<>())
锁机制 写操作时加 ReentrantLock 几乎所有方法都用 synchronized 所有方法都用 synchronized 块包裹
锁粒度 只锁写操作 锁整个对象(方法级别) 锁整个对象(代码块级别)
读性能 极高 (无锁) 差 (加锁) 差 (加锁)
写性能 (需要复制数组) 一般 (加锁,但无复制成本) 一般 (加锁,但无复制成本)
迭代器安全 绝对安全 (不会抛 ConcurrentModificationException) 不安全 (需要手动加锁) 不安全 (需要手动加锁)
数据一致性 弱一致性/最终一致性 (读到快照) 强一致性 强一致性
最佳场景 读多写少 (不推荐使用,性能差) 需要一个简单的线程安全List,且读写均衡

6. 代码示例

下面的例子展示了 CopyOnWriteArrayList 如何在多线程环境中安全工作,并且其迭代器不会抛出异常。

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

public class CopyOnWriteArrayListExample {

    public static void main(String[] args) {
        // 1. 创建一个 CopyOnWriteArrayList
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        // 2. 创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 3. 启动一个写线程,它会不断向列表中添加元素
        executorService.submit(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(100); // 慢一点,让读线程有机会看到变化
                    String element = "New-" + i;
                    list.add(element);
                    System.out.println(Thread.currentThread().getName() + " added: " + element);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 4. 启动多个读线程,它们会遍历列表
        for (int i = 0; i < 3; i++) {
            executorService.submit(() -> {
                try {
                    // 获取迭代器时,拿到的是当时列表的快照
                    for (String item : list) {
                        System.out.println(Thread.currentThread().getName() + " is reading: " + item);
                        Thread.sleep(50); // 模拟耗时操作
                    }
                    // 即使在遍历过程中,写线程添加了新元素,这里也不会抛出 ConcurrentModificationException
                    System.out.println(Thread.currentThread().getName() + " finished reading.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        executorService.shutdown();
    }
}

运行结果分析:

你会看到,读线程在遍历列表的同时,写线程也在添加新元素。

  • 读线程的遍历过程不会被中断,也不会抛出 ConcurrentModificationException
  • 某个读线程开始遍历时,它只能看到那个时刻列表中的元素。即使在它遍历的中途,写线程加入了新元素,这个读线程的当前这次遍历也看不到新加入的元素。这就是“快照”的体现。

总结

CopyOnWriteArrayList 是一个用空间换时间的并发容器。它通过在写操作时付出昂贵的复制成本,来换取读操作的无锁和高性能。

记住它的核心权衡:

为了极致的读性能和无锁的迭代器安全,牺牲了写性能和数据的实时强一致性。

因此,在选择是否使用它时,请务必仔细分析你的业务场景是否符合“读多写少”的特点。

00:00
00:00