并发容器
并发容器是为多线程访问设计的数据结构。它们不是简单地给普通集合所有方法加 synchronized,而是针对不同读写模式做了专门优化。
# 1. 为什么需要并发容器
普通集合并发写入可能出现:
- 数据丢失。
- 结构损坏。
- 迭代异常。
- 可见性问题。
- 死循环或状态不一致。
并发容器提供:
- 线程安全。
- 更高并发度。
- 弱一致性迭代。
- 阻塞协作能力。
- 原子复合操作。
# 2. 常见并发容器
| 容器 | 适合场景 |
|---|---|
ConcurrentHashMap | 高并发 key-value |
ConcurrentSkipListMap | 并发有序 Map |
CopyOnWriteArrayList | 读多写少 List |
BlockingQueue | 生产者消费者 |
ConcurrentLinkedQueue | 非阻塞并发队列 |
ConcurrentLinkedDeque | 非阻塞并发双端队列 |
ArrayBlockingQueue | 有界阻塞队列 |
LinkedBlockingQueue | 链表阻塞队列 |
# 3. 同步包装与并发容器
Map<K, V> syncMap = Collections.synchronizedMap(new HashMap<>());
Map<K, V> concurrentMap = new ConcurrentHashMap<>();
对比:
| 对比项 | 同步包装 | 并发容器 |
|---|---|---|
| 实现 | 外层方法同步 | 内部并发算法 |
| 并发度 | 较低 | 较高 |
| 复合操作 | 仍需外部同步 | 提供原子方法 |
| 迭代 | 需要手动同步 | 多为弱一致性 |
简单低并发可以用同步包装,高并发优先并发容器。
# 4. 弱一致性迭代
ConcurrentHashMap 迭代时不会抛 ConcurrentModificationException,但也不保证看到遍历期间的所有修改。
迭代开始
│
├─ 可能看到旧元素
├─ 可能看到部分新元素
└─ 不会因并发修改 fail-fast
这叫弱一致性。适合监控、统计、遍历处理等不要求强一致快照的场景。
如果需要严格快照,应自己复制:
Map<K, V> snapshot = new HashMap<>(concurrentMap);
# 5. 原子复合操作
并发容器提供一些原子方法:
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> new Value());
map.replace(key, oldValue, newValue);
map.remove(key, value);
不要写:
if (!map.containsKey(key)) {
map.put(key, value);
}
这是非原子的。多线程下可能重复创建或覆盖。
# 6. CopyOnWrite 思想
CopyOnWriteArrayList 写时复制:
读线程 -> 读旧数组,无锁
写线程 -> 复制新数组 -> 修改 -> 替换引用
适合读多写少,例如配置监听器列表、白名单列表。不适合高频写入。
# 7. BlockingQueue 思想
阻塞队列把集合和线程通信结合起来:
队列空:take 阻塞
队列满:put 阻塞
它常用于生产者消费者,能自然提供反压。
# 8. 选型速查
| 需求 | 推荐 |
|---|---|
| 并发 Map | ConcurrentHashMap |
| 并发排序 Map | ConcurrentSkipListMap |
| 并发 Set | ConcurrentHashMap.newKeySet() |
| 读多写少 List | CopyOnWriteArrayList |
| 有界任务队列 | ArrayBlockingQueue |
| 非阻塞无界队列 | ConcurrentLinkedQueue |
| 生产消费阻塞 | BlockingQueue |
# 专家实践与边界
并发容器保证的是容器内部结构在并发访问下不被破坏,但不自动保证业务级复合操作正确。资深开发者会把“单方法线程安全”和“业务事务安全”分开看。
并发容器单次方法
└─ 通常线程安全
业务复合流程
├─ contains
├─ put
└─ update other state
需要额外原子性设计
| 场景 | 推荐容器 | 注意点 |
|---|---|---|
| 高并发 Map | ConcurrentHashMap | 复合操作用原子方法 |
| 读多写少 List | CopyOnWriteArrayList | 写成本高 |
| 生产消费 | BlockingQueue | 容量和超时 |
| 高并发计数 | LongAdder | 结果读取非强一致瞬时值 |
| 有序并发跳表 | ConcurrentSkipListMap | 成本高于普通 Map |
使用并发容器仍要避免在回调中做慢操作,例如 computeIfAbsent 的 mapping 函数里访问远程服务,可能扩大锁/竞争窗口。
# Tips 快问快答
Q:并发容器和同步包装有什么区别? A:同步包装是粗粒度同步,并发容器通常有更细粒度或无锁设计。
Q:ConcurrentHashMap 迭代强一致吗? A:不是,是弱一致性。
Q:并发 Map 中 putIfAbsent 有什么价值? A:把检查和放入做成原子操作。
Q:读多写少 List 用什么?
A:CopyOnWriteArrayList。
Q:生产者消费者用什么?
A:BlockingQueue。
Q:并发 Set 怎么创建?
A:使用 ConcurrentHashMap.newKeySet()。
Q:并发容器就不需要考虑业务一致性了吗? A:仍然需要。容器只保证自身结构安全,不保证跨对象业务事务。
Q:弱一致性迭代会抛并发修改异常吗? A:通常不会,但不保证看到所有最新修改。