Skip to content

并发容器

1. 这是什么

并发容器是为了在多线程环境下安全读写数据而设计的集合工具。
它们通常通过分段、CAS、读写分离、复制等策略,在安全性和性能之间做平衡。

一句话理解:

  • 普通容器关注“功能正确”
  • 并发容器还要额外解决“多线程同时操作时怎么不乱”

2. 为什么重要

普通集合在并发环境下很容易出现这些问题:

  • 数据覆盖
  • 读到不一致结果
  • 迭代时报错
  • 容器内部结构损坏

并发容器就是为了解决这些高频场景而设计的。
但它们不是免费午餐,每种容器都有自己的代价和适用边界。

3. 先建立直觉:并发容器不是“线程安全版普通集合”这么简单

虽然名字上看像是:

  • HashMap -> ConcurrentHashMap
  • ArrayList -> CopyOnWriteArrayList

但它们不只是“多了个锁”。

不同并发容器背后往往用了不同策略:

容器主要策略
ConcurrentHashMap更细粒度并发控制 + CAS
CopyOnWriteArrayList写时复制,读写分离
BlockingQueue阻塞等待 + 容量控制

所以真正要学会的是:

  • 每个容器在解决什么问题
  • 它通过什么机制解决
  • 它因此付出了什么代价

4. 核心内容

4.1 ConcurrentHashMap

ConcurrentHashMap 是最常用的并发键值容器。
它适合:

  • 多线程共享读写映射数据
  • 需要比 Collections.synchronizedMap 更好的并发表现

它的核心价值不是“绝对无锁”,而是:

  • 尽量降低全表级别竞争
  • 提供更合理的线程安全能力

4.2 ConcurrentHashMapHashMap 的差异

最重要的差异有几个:

  • 它是线程安全的
  • 不允许 null key 和 null value
  • 更适合高并发更新场景

为什么不允许 null

  • 因为在并发语义里,null 容易让“值不存在”和“值尚未可见”这两种状态难区分

4.3 CopyOnWriteArrayList

CopyOnWriteArrayList 的思路非常直接:

  • 读操作读旧数组
  • 写操作复制一份新数组再修改

这意味着:

  • 读非常轻量
  • 迭代时不容易受并发写入影响
  • 写操作成本高

所以它特别适合:

  • 读远多于写
  • 元素量不大
  • 需要遍历期间稳定快照

不适合:

  • 高频写入
  • 大列表频繁修改

4.4 快照语义怎么理解

CopyOnWriteArrayList 的一个核心特点是快照迭代。
也就是说:

  • 你拿到迭代器时,看到的是当时那一份数组快照
  • 后续别的线程写入,不会影响这个迭代器正在看到的内容

这既是优点,也是它的语义特点:

  • 你读到的可能不是“实时最新值”
  • 而是“某个时刻的一致快照”

4.5 BlockingQueue

阻塞队列是并发系统里非常重要的一类工具。
它的价值不只是“线程安全队列”,更重要的是:

  • 队列空时,消费者可以阻塞等待
  • 队列满时,生产者也可以阻塞等待

这使它非常适合:

  • 生产者消费者模型
  • 异步解耦
  • 削峰填谷
  • 线程池任务排队

常见实现包括:

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue

4.6 不同阻塞队列的大致特点

ArrayBlockingQueue

  • 有界
  • 基于数组
  • 容量固定

适合:

  • 希望明确限制队列容量

LinkedBlockingQueue

  • 基于链表
  • 常见实现中容量可设很大

适合:

  • 一般生产者消费者场景

但也要注意:

  • 队列太大可能掩盖上游流量问题

SynchronousQueue

  • 不存储元素
  • 生产者放一个,必须立刻被消费者接走

它更像:

  • 线程之间直接交接任务

4.7 并发容器的共同代价

线程安全不是免费获得的。
常见代价包括:

  • 写入开销更高
  • 内存占用更高
  • 一致性语义和普通容器不同
  • 某些场景吞吐并不会更好

所以选择并发容器,不能只看“线程安全”四个字。

5. 学习重点

这一章最重要的是理解:

  • 并发容器通过不同策略实现安全
  • ConcurrentHashMap 适合共享映射更新
  • CopyOnWriteArrayList 的关键是读多写少和快照语义
  • BlockingQueue 的关键是阻塞协调和容量控制
  • 并发容器不是任何场景都优于普通容器

6. 常见问题

6.1 在高并发场景直接使用普通 HashMap

这会让系统处于不受保护的状态。
不是“偶尔有点风险”,而是语义上就不安全。

6.2 滥用 CopyOnWriteArrayList

如果写很多、列表很大,复制成本会非常明显。

6.3 不理解阻塞队列在生产者消费者场景里的价值

很多人会手写:

  • wait/notify
  • 自己维护列表和锁

而实际上 BlockingQueue 已经把这些核心协作模式封装好了。

7. 动手验证

这一节可以直接复制运行,边看边验证。

7.1 准备一个可运行示例

新建文件 ConcurrentContainerDemo.java,内容如下:

java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConcurrentContainerDemo {
    public static void main(String[] args) throws Exception {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 4; i++) {
            pool.submit(() -> {
                for (int j = 0; j < 1000; j++) {
                    map.merge("count", 1, Integer::sum);
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("concurrentMapCount=" + map.get("count"));

        try {
            map.put("x", null);
        } catch (NullPointerException e) {
            System.out.println("concurrentMapNullRejected=NullPointerException");
        }

        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("A");
        cowList.add("B");
        Iterator<String> iterator = cowList.iterator();
        cowList.add("C");
        List<String> snapshot = new ArrayList<>();
        while (iterator.hasNext()) {
            snapshot.add(iterator.next());
        }
        System.out.println("cowSnapshot=" + snapshot);
        System.out.println("cowCurrentList=" + cowList);

        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
        Thread consumer = new Thread(() -> {
            try {
                String item = queue.take();
                System.out.println("blockingQueueTaken=" + item);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        consumer.start();
        Thread.sleep(100);
        queue.put("task-1");
        consumer.join();
        System.out.println("blockingQueueSize=" + queue.size());
    }
}

7.2 编译并运行

bash
javac ConcurrentContainerDemo.java
java ConcurrentContainerDemo

如果你在 PowerShell 中操作,也直接执行同样两条命令即可。

7.3 你应该观察到什么

输出不一定逐字完全一致,但应包含这些关键信息:

text
concurrentMapCount=4000
concurrentMapNullRejected=NullPointerException
cowSnapshot=[A, B]
cowCurrentList=[A, B, C]
blockingQueueTaken=task-1
blockingQueueSize=0

7.4 每一行在验证什么

  • concurrentMapCount=4000:说明 ConcurrentHashMap 能支持多线程安全更新
  • concurrentMapNullRejected=NullPointerException:说明它不允许 null
  • cowSnapshot=[A, B]:说明 CopyOnWriteArrayList 的迭代器拿到的是旧快照
  • cowCurrentList=[A, B, C]:说明写入已经生效,但不会影响旧迭代器视图
  • blockingQueueTaken=task-1:说明阻塞队列能让消费者等待任务到来
  • blockingQueueSize=0:说明任务被消费后队列恢复为空

7.5 再做两个延伸验证

你可以继续做下面两个实验:

  1. CopyOnWriteArrayList 连续加入很多写操作
  2. ArrayBlockingQueue<>(1) 改成更大容量

你可以观察:

  • 写时复制的代价会越来越明显
  • 队列容量会直接影响生产者消费者的节奏和缓冲能力

8. 练习建议

下面这些练习做完,这一章会更扎实:

  • 比较 HashMapConcurrentHashMap 的使用边界
  • 用阻塞队列实现一个最小生产者消费者模型
  • 总结常见并发容器的选型表
  • 对比 CopyOnWriteArrayList 与普通 ArrayList 在迭代场景下的行为差异

9. 自测问题

  • ConcurrentHashMap 为什么更适合并发场景?
  • CopyOnWriteArrayList 适合什么类型的业务?
  • 阻塞队列在并发系统里常用来做什么?
  • 为什么并发容器的线程安全不是免费获得的?
  • 快照迭代和实时视图有什么区别?

10. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • ConcurrentHashMap 适合多线程共享更新映射数据
  • CopyOnWriteArrayList 适合读多写少和快照读取场景
  • BlockingQueue 同时承担线程安全和协作节奏控制作用
  • 并发容器的实现策略不同,代价也不同
  • 选择并发容器时要看读写比例、容量需求和一致性语义