了解并发容器 CopyOnWriteArrayList

了解并发容器 CopyOnWriteArrayList

一、CopyOnWriteArrayList 概述

  1. 什么是 CopyOnWriteArrayList?

    • Java 并发包 (java.util.concurrent) 中的线程安全容器
    • 基于「写时复制」(Copy-On-Write)机制实现
    • ArrayList 的核心区别:读写分离的线程安全策略
  2. 设计目标与适用场景

    • 解决多线程环境下遍历与修改的冲突
    • 读多写少场景的高效实现(如:事件监听器列表、配置管理)
  3. 与普通 ArrayList 的对比

    • 线程安全性差异(ArrayList 非线程安全)
    • 迭代器行为区别(CopyOnWriteArrayList 迭代器不抛出 ConcurrentModificationException
对比维度 CopyOnWriteArrayList ArrayList
线程安全性 ✅ 线程安全(基于写时复制和锁机制) ❌ 非线程安全(需外部同步如 Collections.synchronizedList
底层实现 写操作时复制新数组(volatile 数组 + ReentrantLock 动态数组(Object[] 无锁,依赖 modCount 机制)
修改操作 写操作加锁,每次修改都会复制整个数组(O(n) 时间) 直接修改原数组(O(1) 追加,O(n) 插入/删除)
迭代器行为 弱一致性:迭代器遍历创建时的数组快照,不抛异常 快速失败(ConcurrentModificationException
读操作性能 ⚡ 极高(无锁,直接访问数组) ⚡ 高(但多线程需同步时性能下降)
写操作性能 ⚠️ 低(需复制数组,大数据量时内存和性能开销大) ⚡ 高(无复制,但多线程需同步时性能下降)
内存开销 高(每次写操作复制数组,旧数组可能未被及时 GC) 低(直接操作原数组)
适用场景 读多写少(如事件监听器、黑白名单) 单线程环境,或需手动控制同步的多线程环境
异常风险 迭代器不会抛出 ConcurrentModificationException 并发修改时迭代器可能抛出异常
批量操作优化 批量写入建议用 addAll 减少复制次数 无特殊优化,直接操作原数组

二、内部实现原理

CopyOnWriteArrayList 的线程安全是通过“写时复制机制”实现的。CopyOnWriteArrayList 的写操作通过 锁(ReentrantLock)volatile 数组引用 保证线程安全。

  1. 写时复制(Copy-On-Write)机制

    • 写入操作(add/set/remove):复制底层数组 → 修改副本 → 替换原数组
    • 读取操作:直接访问原数组(无锁)
  2. 底层数据结构

    • 使用 volatile 数组引用(private transient volatile Object[] array;),保证多线程下数据的可见性。
    • 通过 ReentrantLock 保证写操作的原子性。同一时间只有一个线程能持有锁,其他线程必须等待锁释放。
  3. 线程安全性实现

    • 写操作加锁(避免多个线程同时修改)
    • 读操作无锁(依赖数组引用的 volatile 特性)

三、核心 API 与使用示例

  1. 构造函数

    • CopyOnWriteArrayList():创建空列表
    • CopyOnWriteArrayList(Collection<? extends E> c):从集合初始化
  2. 常用方法

    • 写操作:add(), addIfAbsent(), remove(), set()
    • 读操作:get(), size(), iterator()
    • 批量操作:addAllAbsent(), removeAll()
  3. api使用示例

    // 创建实例
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    
    // 多线程添加元素
    Runnable writer = () -> list.add(Thread.currentThread().getName());
    new Thread(writer).start();
    new Thread(writer).start();
    
    // 安全遍历(迭代器访问旧数组)
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        System.out.println(it.next());
    }
    

四、代码示例说明写时复制机制

场景:两个线程同时向 CopyOnWriteArrayList 添加元素。

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

// 线程1:添加元素 "A"
Thread t1 = new Thread(() -> {
    list.add("A");
    System.out.println("线程1添加完成,当前数组:" + list);
});

// 线程2:添加元素 "B"
Thread t2 = new Thread(() -> {
    list.add("B");
    System.out.println("线程2添加完成,当前数组:" + list);
});

t1.start();
t2.start();

// 输出可能:
// 线程1添加完成,当前数组:[A]
// 线程2添加完成,当前数组:[A, B]

2. 流程图与代码执行步骤对照

sequenceDiagram participant 线程1 participant 锁 participant 数组 participant 线程2 线程1->>锁: 请求加锁(成功) 线程1->>数组: 复制原数组 [] → 新数组 ['A'] 线程1->>数组: 替换引用为 ['A'] 线程1->>锁: 释放锁 线程2->>锁: 请求加锁(阻塞等待...) 线程2->>锁: 加锁成功(获取到线程1修改后的数组 ['A']) 线程2->>数组: 复制新数组 ['A'] → ['A', 'B'] 线程2->>数组: 替换引用为 ['A', 'B'] 线程2->>锁: 释放锁

步骤1. 线程1先获取锁

// 线程1执行 add("A")
final ReentrantLock lock = this.lock;
lock.lock(); // 线程1获取锁成功
try {
    Object[] elements = getArray(); // 原数组为 []
    Object[] newElements = Arrays.copyOf(elements, elements.length + 1); // 复制为 ["A"]
    newElements[elements.length] = "A";
    setArray(newElements); // 替换数组引用为新数组 ["A"]
} finally {
    lock.unlock(); // 释放锁
}

步骤2. 线程2等待锁释放

  • 线程2调用 add("B") 时,发现锁已被线程1持有,进入阻塞状态。
  • 直到线程1释放锁后,线程2才能继续执行。

步骤3. 线程2获取锁并操作

// 线程2执行 add("B")
lock.lock(); // 线程1释放锁后,线程2获取锁成功
try {
    Object[] elements = getArray(); // 此时原数组是线程1修改后的 ["A"]
    Object[] newElements = Arrays.copyOf(elements, elements.length + 1); // 复制为 ["A", "B"]
    newElements[elements.length] = "B";
    setArray(newElements); // 替换数组引用为新数组 ["A", "B"]
} finally {
    lock.unlock();
}

锁机制 保证了写操作的原子性和串行性,volatile 数组引用 确保线程间的修改可见性。因此,线程2的写操作总是在线程1的修改结果基础上进行,不会出现数据覆盖或丢失。


五、总结

CopyOnWriteArrayList 写时复制机制特性如下:

  • 写操作原子性:通过锁保证同一时间只有一个线程修改数组。
  • 数据隔离性:读操作始终访问旧数组,直到写操作完成替换。
  • 内存可见性volatile 修饰的数组引用确保修改对其他线程立即可见。

写时复制机制确保了 CopyOnWriteArrayList读操作无锁且线程安全,但 写操作成本高昂,需根据场景权衡使用。

posted @ 2025-04-02 22:21  皮皮是个不挑食的好孩子  阅读(62)  评论(0)    收藏  举报