并发容器
用途:多线程共享集合时选择正确容器,避免“偶发数据错乱”
选择表
「选择表」小节链接| 场景 | 选择 | 说明 |
|---|---|---|
| 高并发读写 Map | ConcurrentHashMap |
分段/CAS + 细粒度锁,读无锁 |
| 需要顺序的并发队列 | ConcurrentLinkedQueue(无锁)/ LinkedBlockingQueue(锁 + 有界) |
有界队列可做背压 |
| 阻塞取任务 | ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue |
线程池工作队列基础 |
| 优先级 + 超时 | PriorityBlockingQueue |
无界,注意内存 |
| 延迟执行 | DelayQueue |
定时任务、超时关闭 |
| 读多写少、快照读 | CopyOnWriteArrayList |
写时复制,写代价高,禁止大集合频繁写 |
| 高频计数 | LongAdder |
优于 ConcurrentHashMap 计数 |
| 并发流处理 | parallelStream + 共享 ForkJoinPool |
IO 任务不适合(池共享) |
ConcurrentHashMap 的关键语义
「ConcurrentHashMap 的关键语义」小节链接- 单个操作原子(
put、putIfAbsent、compute、merge、computeIfAbsent),但**“先 get 再 put”的组合不原子**——必须用computeIfAbsent/merge表达。 - 迭代器弱一致:不抛
ConcurrentModificationException,但可能看到也可能看不到并发修改,不适合做“精确快照统计”。 - 键值不得为 null:无法区分“不存在”与“值为 null”。
size()是估算:并发写时不是精确值;需要精确计数用累加器或外部计数。computeIfAbsent内不要做远程调用或递归修改同一 map:可能长时间持锁或死锁。
阻塞队列与生产者消费者
「阻塞队列与生产者消费者」小节链接- 用
offer(timeout)/poll(timeout)而不是无限put,避免上游被永久阻塞。 - 队列长度是背压信号:持续接近容量说明消费能力不足,应扩容消费者或拒绝部分请求,而不是加大队列(只是把延迟藏起来)。
- 关闭时先停止生产,再消费完剩余(
poison pill或shutdown+awaitTermination)。
旧同步容器的现状
「旧同步容器的现状」小节链接Collections.synchronizedMap、Hashtable、Vector 用整体互斥,扩展性差,新代码应使用并发容器;synchronizedList 迭代仍需手动加锁。
常见误区
「常见误区」小节链接| 误区 | 修正 |
|---|---|
用 ConcurrentHashMap 仍写 if(!map.containsKey(k)) map.put(...) |
用 computeIfAbsent / merge |
用 CopyOnWriteArrayList 承载高频写 |
改读写锁或分片结构 |
| 无界队列当“不会丢数据” | 内存耗尽或延迟不可控,必须设上限与拒绝 |
依赖 size() 做精确限流 |
用独立计数器或令牌桶 |
在 computeIfAbsent 里调用 RPC |
锁持有时间不可控,改为外部预取或异步加载 |
延伸阅读
「延伸阅读」小节链接- 《Java 并发编程实战》第 5 章(并发容器与同步队列)
- Doug Lea《JSR-166 Interest Guide》
- OpenJDK
java.util.concurrent包文档