Skip to content

ArrayList 线程安全问题:多线程下的隐形陷阱

一句话警告

ArrayList 是非线程安全的。在多线程环境下使用它,就像在没有红绿灯的十字路口开车——偶尔可能没事,但迟早会出事。


三个经典的线程安全问题

问题 1:数据丢失

两个线程同时 add(),可能互相覆盖:

java
ArrayList<Integer> list = new ArrayList<>();

// 线程 A
list.add(1);

// 线程 B
list.add(2);

// 期望 size = 2
// 实际?可能 size = 1!

具体发生了什么:

java
// 线程 A 执行 add
elementData[size++] = 1;
//                    ↑
//                    size 刚被读取,还没来得及递增
//                    线程 B 也读取了同样的 size

// 线程 B 执行 add
elementData[size++] = 2;
//                    ↑
//                    线程 B 用的 size 和线程 A 相同
//                    结果:线程 B 覆盖了线程 A 的元素

问题 2:size 不准确

即使没有覆盖,size 也可能错乱:

java
// 线程 A: size++ 分解为 3 步
int temp = size;  // 读取
temp = temp + 1;  // 加 1
size = temp;       // 写回

// 如果线程 A 还没写回,线程 B 也读了 size
// 两个线程都读到 0,都写回 1
// 实际加了 2 个元素,但 size = 1

问题 3:数组越界(扩容竞争)

最危险的情况:

java
// 两个线程同时触发扩容
// 线程 A 和 B 都发现容量不够,开始扩容
// 扩容时需要复制数组
// 但如果操作交叉执行,可能导致数组下标越界

并发问题演示

java
import java.util.*;
import java.util.concurrent.*;

public class ConcurrentProblemDemo {

    public static void main(String[] args) throws InterruptedException {
        int threads = 100;
        int perThread = 10000;

        // 测试数据丢失
        ArrayList<Integer> unsafe = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(threads);

        for (int i = 0; i < threads; i++) {
            new Thread(() -> {
                for (int j = 0; j < perThread; j++) {
                    unsafe.add(j);
                }
                latch.countDown();
            }).start();
        }

        latch.await();

        int expected = threads * perThread;
        int actual = unsafe.size();
        System.out.println("预期: " + expected);
        System.out.println("实际: " + actual);
        System.out.println("丢失: " + (expected - actual));
    }
}

典型输出:

预期: 1000000
实际: 973451
丢失: 26549

丢失了几万个元素,而且每次运行结果都不一样。


三种解决方案

方案 1:Collections.synchronizedList

包装一层同步,所有操作都加锁:

java
List<String> safe = Collections.synchronizedList(new ArrayList<>());

safe.add("a");
safe.add("b");
// 所有操作都是线程安全的

特点:

  • ✅ 线程安全
  • ❌ 所有操作都加锁,高并发下性能差
  • ❌ 迭代时仍需要额外同步
java
// 迭代时必须手动同步
synchronized (safe) {
    for (String s : safe) {
        System.out.println(s);
    }
}

方案 2:CopyOnWriteArrayList

写时复制。写操作会复制整个数组,读操作不加锁:

java
List<String> cow = new CopyOnWriteArrayList<>();

cow.add("a");
cow.add("b");

// 读操作,无需加锁
for (String s : cow) {
    System.out.println(s);
}

特点:

  • ✅ 读操作完全无锁,极快
  • ✅ 迭代安全,不会抛出 ConcurrentModificationException
  • ❌ 写操作需要复制整个数组,慢
  • ❌ 读到的可能是旧数据(最终一致性)

适用场景:读多写少(读占比 90% 以上)。

方案 3:使用 ConcurrentHashMap 存索引

如果需要并发写入元素:

java
// 把 ArrayList 换成 ConcurrentHashMap
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
AtomicInteger counter = new AtomicInteger(0);

// 多线程安全地添加
for (int i = 0; i < 100; i++) {
    map.put(counter.incrementAndGet(), "value" + i);
}

性能对比

java
import java.util.*;
import java.util.concurrent.*;

public class ThreadSafePerformanceDemo {

    public static void main(String[] args) throws Exception {
        int threads = 10;
        int ops = 100_000;

        // ArrayList(非安全)
        System.out.println("ArrayList: " + benchmark(
            () -> new ArrayList<Integer>(), threads, ops));

        // Collections.synchronizedList
        System.out.println("synchronizedList: " + benchmark(
            () -> Collections.synchronizedList(new ArrayList<Integer>()), threads, ops));

        // CopyOnWriteArrayList
        System.out.println("CopyOnWriteArrayList: " + benchmark(
            () -> new CopyOnWriteArrayList<Integer>(), threads, ops));
    }

    static String benchmark(Supplier<List<Integer>> factory, int threads, int ops)
            throws Exception {
        List<List<Integer>> lists = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(threads);

        long start = System.nanoTime();
        for (int t = 0; t < threads; t++) {
            new Thread(() -> {
                List<Integer> list = factory.get();
                for (int i = 0; i < ops; i++) {
                    list.add(i);
                }
                lists.add(list);
                latch.countDown();
            }).start();
        }
        latch.await();
        return (System.nanoTime() - start) / 1_000_000 + " ms";
    }
}

典型结果(10 线程,每线程 10 万次添加):

ArrayList: ~200 ms(但有数据丢失)
synchronizedList: ~800 ms
CopyOnWriteArrayList: ~3000 ms(写操作复制数组)

避坑指南

误区正确做法
「ArrayList 不会出问题,因为我没遇到过」偶发性 bug更难排查,多线程下必出问题
「加了 synchronized 就高枕无忧了」synchronizedList 迭代时还需手动同步
「CopyOnWrite 适合所有并发场景」只适合读多写少(90% 以上读)
「ConcurrentHashMap 比 Hashtable 快」ConcurrentHashMap 快 5-10 倍,且更现代

总结:选哪个?

单线程 → ArrayList
多线程,读多写少 → CopyOnWriteArrayList
多线程,写操作多 → Collections.synchronizedList
或者,换个思路:用 ConcurrentHashMap 代替

相关链接

基于 VitePress 构建