并发04:并发容器

从 ConcurrentHashMap 的分段锁到 CopyOnWriteArrayList 的读写分离,再到 BlockingQueue 的生产消费模式,系统理解 Java 并发容器的设计思想和适用场景。

字数 1326 阅读时长 ≈ 4 分钟 2026-5-29 2026-7-12
并发04:并发容器

并发容器不是加把锁这么简单

并发容器不是”普通集合外面包一把锁”。它真正做的是在正确性、吞吐、内存成本和迭代语义之间重新取舍。

并发容器选择路径

比如 Collections.synchronizedMap(new HashMap<>()) 确实能用互斥锁保护 Map,但所有读写都抢同一把锁,迭代时还要额外手动同步。ConcurrentHashMap 则通过更细的并发控制和 CAS,把常见读写路径做得更适合高并发。

项目里选并发容器,不能只看名字里有没有 Concurrent,要看读写比例、是否需要阻塞等待、迭代结果是否必须强一致。

ConcurrentHashMap 的并发控制

ConcurrentHashMap 常用在本地缓存、连接映射、任务状态表这类场景。Java 8 之后,它不再使用早期的 Segment 分段锁模型,而是围绕数组、链表/红黑树、CAS 和节点级 synchronized 做并发控制。

版本锁策略特点
Java 7Segment 分段锁多个 Segment 并发,每个 Segment 互斥
Java 8+CAS + 节点级锁更细粒度,读写并发更高

它的一个重要能力是复合原子方法:

cache.computeIfAbsent(key, k -> loadFromDb(k));

这比先 getput 更稳,因为”没有就创建”这个动作被容器封装成了原子语义。

扩容和树化机制

ConcurrentHashMap 仍然有哈希桶、链表和红黑树这些结构,但并发场景下最难的是扩容。扩容不能简单停住全世界,所以 Java 8 之后会让多个线程协助迁移桶数据。

当某个桶冲突很高时,链表会树化成红黑树,以降低查询复杂度。但树化不是只看链表长度,还和数组容量有关:容量太小时,优先扩容比树化更划算。

不过它也不是万能缓存。size() 在并发修改时只能尽力统计,不适合做强一致业务判断。用它判断”当前元素是否精确超过阈值”要非常谨慎。

弱一致迭代

ConcurrentHashMap 的迭代器是弱一致的。迭代时其他线程可以继续修改 Map,迭代器不会像 ArrayList 那样轻易抛 ConcurrentModificationException,但它也不保证你看到的是某个瞬间的完整快照。

这适合监控、统计、清理过期项这类场景:允许轻微误差,换取系统继续运行。

如果业务要求”这次遍历必须覆盖当前所有元素且不能多不能少”,那就不要直接依赖并发迭代语义。可以复制快照、加锁、用数据库分页,或者重新设计状态边界。

CopyOnWriteArrayList 读写分离

CopyOnWriteArrayList 的思路很直白:写入时复制一份新数组,改完后替换引用;读取时不加锁,读旧数组也没关系。

CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();

for (Listener listener : listeners) {
    listener.onEvent(event);
}
场景适用不适用
监听器列表-
配置快照-
黑白名单-
高频写入的消息列表-

它不适合高频写入。每次写都复制数组,元素越多成本越高。把它拿来做高并发消息列表,内存和 CPU 都会很难看。

BlockingQueue 生产消费边界

BlockingQueue 不只是线程安全队列,它还表达了生产者和消费者之间的背压关系。

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);
queue.put(task);
Task task = queue.take();

队列满了,生产者等待或失败;队列空了,消费者等待。这让系统有机会把压力停在边界上,而不是无限创建任务。

队列选择就是压力模型选择

队列类型特点适用场景
ArrayBlockingQueue容量固定,边界清楚稳定服务的任务缓冲
LinkedBlockingQueue默认容量很大需要容量时必须显式设置
SynchronousQueue不存元素,直接交接任务必须立即执行
PriorityBlockingQueue按优先级排序,默认无界需要优先级控制,注意容量
DelayQueue延迟任务定时任务场景

线程池里的工作队列,本质上也在做这件事。有界队列能让容量显性化,无界队列会把压力藏起来,最后变成内存压力和超时堆积。

常见误区

误区一:并发容器里放什么操作都安全

容器只能保证自己的结构安全,保证不了元素对象内部状态安全。Map 里放了一个普通 ArrayList,多个线程同时改这个 List,仍然会出问题。

误区二:线程安全方法组合起来就线程安全

containsKey 后再 put 不是整体原子,要用 putIfAbsentcomputeIfAbsent 这类复合方法。

误区三:所有场景都用 ConcurrentHashMap

读多写少的小列表可能更适合 CopyOnWrite;需要等待和削峰的场景更适合 BlockingQueue;需要顺序和优先级时还要看具体队列。

项目里怎么选

选并发容器时先问:

  1. 数据主要是读多、写多,还是生产消费
  2. 是否需要阻塞等待、超时放弃或有界背压
  3. 迭代结果是否必须强一致
  4. 容器里的元素自身是否也是线程安全的
  5. 是否需要复合原子操作,而不是多个方法拼起来?

如果只是高并发 Key-Value 映射,优先看 ConcurrentHashMap;如果是监听器和配置快照,考虑 CopyOnWriteArrayList;如果是任务交接和削峰,先看有界 BlockingQueue

并发容器的价值不是省掉思考,而是把常见并发访问模式做成更可靠的边界。