Skip to content

CopyOnWriteArrayList

读多写少的场景,用什么 List?

synchronizedList 性能差,Vector 同样有锁。有没有读操作无锁的方案?

CopyOnWriteArrayList:写时复制,读操作完全无锁。

核心原理

┌──────────────────────────────────────────────────┐
│            CopyOnWriteArrayList 原理               │
├──────────────────────────────────────────────────┤
│                                                  │
│  写操作:                                          │
│  1. 复制整个数组                                    │
│  2. 在副本上修改                                   │
│  3. 用新数组替换旧数组(原子操作)                  │
│                                                  │
│  ┌─────────────┐                                │
│  │  旧数组      │  ← 读操作继续用这个              │
│  └─────────────┘                                │
│  ┌─────────────┐                                │
│  │  新数组      │  ← 写操作创建的新副本             │
│  └─────────────┘                                │
│                                                  │
│  读操作:无锁,直接读取                            │
│                                                  │
└──────────────────────────────────────────────────┘

写操作的本质:

java
// 写操作简化逻辑
public boolean add(E e) {
    synchronized (lock) {
        Object[] newElements = Arrays.copyOf(elements, elements.length + 1);
        newElements[elements.length] = e;
        elements = newElements;  // 原子替换
    }
}

基础用法

java
public class CopyOnWriteDemo {

    private static final CopyOnWriteArrayList<String> list =
        new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        // 添加
        list.add("元素1");
        list.add("元素2");
        list.addIfAbsent("元素1");  // 不重复添加

        System.out.println("列表: " + list);

        // 读取(无锁)
        for (String elem : list) {
            System.out.println("读取: " + elem);
        }

        // 遍历过程中,其他线程的修改可能看到或看不到
        // 但不会抛出 ConcurrentModificationException

        // 修改
        list.set(0, "新元素");
        System.out.println("修改后: " + list);
    }
}

弱一致性:迭代器的特点

java
public class WeakConsistencyDemo {

    private static final CopyOnWriteArrayList<Integer> list =
        new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        // 迭代器持有的是创建时的快照
        Iterator<Integer> iterator = list.iterator();

        // 迭代过程中修改
        list.add(100);

        // 迭代器看不到新元素(弱一致性)
        System.out.println("迭代器看到的: ");
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }

        // 但 list 本身有新元素
        System.out.println("\nlist 实际: " + list);
    }
}

输出可能:

迭代器看到的: 0 1 2 3 4 5 6 7 8 9
list 实际: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100]

迭代器不会 ConcurrentModificationException,因为它操作的是快照。

读多写少场景演示

java
public class ReadHeavyDemo {

    private static final CopyOnWriteArrayList<String> cache =
        new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 填充数据
        for (int i = 0; i < 1000; i++) {
            cache.add("数据-" + i);
        }

        CountDownLatch latch = new CountDownLatch(100);

        // 100 个读线程
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    // 读操作无锁,高性能
                    cache.contains("数据-" + j);
                }
                latch.countDown();
            }).start();
        }

        // 偶尔写入
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                cache.add("新数据-" + i);
                try {
                    Thread.sleep(100);
                } catch (Exception e) {}
            }
        }).start();

        latch.await();
        System.out.println("最终大小: " + cache.size());
    }
}

CopyOnWriteArraySet

CopyOnWriteArrayList 的 Set 版本:

java
public class CopyOnWriteSetDemo {

    private static final CopyOnWriteArraySet<String> set =
        new CopyOnWriteArraySet<>();

    public static void main(String[] args) {
        set.add("A");
        set.add("B");
        set.add("A");  // 重复不添加

        System.out.println("Set: " + set);
        System.out.println("大小: " + set.size());
    }
}

底层还是 CopyOnWriteArrayList,只是自动去重。

与其他 List 对比

特性CopyOnWriteArrayListsynchronizedListVector
读操作无锁,最高性能有锁有锁
写操作复制整个数组有锁有锁
迭代器弱一致,不抛异常快速失败快速失败
适用场景读多写少写多读少不推荐

使用决策

需要线程安全的 List?

  ├── 读多写少(配置、黑名单、白名单)
  │     └── CopyOnWriteArrayList ✅

  ├── 写多读少(并发写入)
  │     └── Collections.synchronizedList ❌ 性能差
  │     └── JUC 的其他并发 List

  └── 需要遍历时修改
        └── Collections.synchronizedList(注意并发修改)

常见错误

错误一:写操作频繁

java
// ❌ 错误:CopyOnWriteArrayList 不适合写多
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100000; i++) {
    list.add(i);  // 每次都复制数组,极慢!
}

// ✅ 正确:写多时用 synchronizedList
List<Integer> list = Collections.synchronizedList(new ArrayList<>());

错误二:批量操作误用

java
// ❌ 错误:批量操作不是原子的
list.add("A");
list.add("B");
list.add("C");

// 迭代器可能在批量操作中间看到不一致状态

// ✅ 正确:需要原子批量操作时,使用 synchronized
synchronized (list) {
    list.add("A");
    list.add("B");
    list.add("C");
}

注意事项

  1. 写操作开销大:复制整个数组,大数据量时内存压力大
  2. 内存占用高:旧数组需要等待 GC
  3. 不适合写多场景:频繁写入时性能差
  4. 迭代器弱一致:可能看不到最新修改
  5. 适合小型数据集:如配置列表、黑白名单

要点回顾

  • CopyOnWriteArrayList 写时复制数组,读操作完全无锁
  • 读多写少场景性能优秀
  • 迭代器弱一致,不抛 ConcurrentModificationException
  • 写操作复制整个数组,开销大,不适合写多场景
  • 适合:配置列表、黑白名单、小型只读缓存

相关链接

基于 VitePress 构建