并发容器
1. 这是什么
并发容器是为了在多线程环境下安全读写数据而设计的集合工具。
它们通常通过分段、CAS、读写分离、复制等策略,在安全性和性能之间做平衡。
一句话理解:
- 普通容器关注“功能正确”
- 并发容器还要额外解决“多线程同时操作时怎么不乱”
2. 为什么重要
普通集合在并发环境下很容易出现这些问题:
- 数据覆盖
- 读到不一致结果
- 迭代时报错
- 容器内部结构损坏
并发容器就是为了解决这些高频场景而设计的。
但它们不是免费午餐,每种容器都有自己的代价和适用边界。
3. 先建立直觉:并发容器不是“线程安全版普通集合”这么简单
虽然名字上看像是:
HashMap->ConcurrentHashMapArrayList->CopyOnWriteArrayList
但它们不只是“多了个锁”。
不同并发容器背后往往用了不同策略:
| 容器 | 主要策略 |
|---|---|
ConcurrentHashMap | 更细粒度并发控制 + CAS |
CopyOnWriteArrayList | 写时复制,读写分离 |
BlockingQueue | 阻塞等待 + 容量控制 |
所以真正要学会的是:
- 每个容器在解决什么问题
- 它通过什么机制解决
- 它因此付出了什么代价
4. 核心内容
4.1 ConcurrentHashMap
ConcurrentHashMap 是最常用的并发键值容器。
它适合:
- 多线程共享读写映射数据
- 需要比
Collections.synchronizedMap更好的并发表现
它的核心价值不是“绝对无锁”,而是:
- 尽量降低全表级别竞争
- 提供更合理的线程安全能力
4.2 ConcurrentHashMap 和 HashMap 的差异
最重要的差异有几个:
- 它是线程安全的
- 不允许
nullkey 和nullvalue - 更适合高并发更新场景
为什么不允许 null?
- 因为在并发语义里,
null容易让“值不存在”和“值尚未可见”这两种状态难区分
4.3 CopyOnWriteArrayList
CopyOnWriteArrayList 的思路非常直接:
- 读操作读旧数组
- 写操作复制一份新数组再修改
这意味着:
- 读非常轻量
- 迭代时不容易受并发写入影响
- 写操作成本高
所以它特别适合:
- 读远多于写
- 元素量不大
- 需要遍历期间稳定快照
不适合:
- 高频写入
- 大列表频繁修改
4.4 快照语义怎么理解
CopyOnWriteArrayList 的一个核心特点是快照迭代。
也就是说:
- 你拿到迭代器时,看到的是当时那一份数组快照
- 后续别的线程写入,不会影响这个迭代器正在看到的内容
这既是优点,也是它的语义特点:
- 你读到的可能不是“实时最新值”
- 而是“某个时刻的一致快照”
4.5 BlockingQueue
阻塞队列是并发系统里非常重要的一类工具。
它的价值不只是“线程安全队列”,更重要的是:
- 队列空时,消费者可以阻塞等待
- 队列满时,生产者也可以阻塞等待
这使它非常适合:
- 生产者消费者模型
- 异步解耦
- 削峰填谷
- 线程池任务排队
常见实现包括:
ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue
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,内容如下:
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 编译并运行
javac ConcurrentContainerDemo.java
java ConcurrentContainerDemo如果你在 PowerShell 中操作,也直接执行同样两条命令即可。
7.3 你应该观察到什么
输出不一定逐字完全一致,但应包含这些关键信息:
concurrentMapCount=4000
concurrentMapNullRejected=NullPointerException
cowSnapshot=[A, B]
cowCurrentList=[A, B, C]
blockingQueueTaken=task-1
blockingQueueSize=07.4 每一行在验证什么
concurrentMapCount=4000:说明ConcurrentHashMap能支持多线程安全更新concurrentMapNullRejected=NullPointerException:说明它不允许null值cowSnapshot=[A, B]:说明CopyOnWriteArrayList的迭代器拿到的是旧快照cowCurrentList=[A, B, C]:说明写入已经生效,但不会影响旧迭代器视图blockingQueueTaken=task-1:说明阻塞队列能让消费者等待任务到来blockingQueueSize=0:说明任务被消费后队列恢复为空
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 给
CopyOnWriteArrayList连续加入很多写操作 - 把
ArrayBlockingQueue<>(1)改成更大容量
你可以观察:
- 写时复制的代价会越来越明显
- 队列容量会直接影响生产者消费者的节奏和缓冲能力
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 比较
HashMap和ConcurrentHashMap的使用边界 - 用阻塞队列实现一个最小生产者消费者模型
- 总结常见并发容器的选型表
- 对比
CopyOnWriteArrayList与普通ArrayList在迭代场景下的行为差异
9. 自测问题
ConcurrentHashMap为什么更适合并发场景?CopyOnWriteArrayList适合什么类型的业务?- 阻塞队列在并发系统里常用来做什么?
- 为什么并发容器的线程安全不是免费获得的?
- 快照迭代和实时视图有什么区别?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
ConcurrentHashMap适合多线程共享更新映射数据CopyOnWriteArrayList适合读多写少和快照读取场景BlockingQueue同时承担线程安全和协作节奏控制作用- 并发容器的实现策略不同,代价也不同
- 选择并发容器时要看读写比例、容量需求和一致性语义