Java并发:CAS、原子类、ConcurrentHashMap与并发容器
前言
并发容器与原子类的重点不在于记住 API,而在于理解无锁更新、竞争成本和复合操作的边界。本文从 CAS、ABA 到 ConcurrentHashMap 的实现和选型展开,并结合计数、缓存和并发访问等生产场景说明何时该使用原子操作,何时仍需显式同步。
CAS 与原子类
CAS(Compare And Swap)含义是“当内存值仍等于预期值时,才更新为新值”。它把比较与写入交给 CPU 原子指令完成,是 Atomic 系列和并发容器实现高并发更新的重要基础。
1 | AtomicInteger counter = new AtomicInteger(); |
CAS 失败通常会重试。低冲突下它避免了线程阻塞;高冲突下大量自旋会浪费 CPU,所以“无锁”不等于永远更快。
ABA 问题
线程 A 看见值为 A,线程 B 将 A 改为 B 又改回 A,A 的 CAS 仍成功,但中间变化可能有业务意义。对只关心数值结果的计数器,ABA 通常无害;对栈顶、库存版本、状态迁移等场景,可用版本号,如 AtomicStampedReference,或直接使用数据库条件更新。
AtomicLong 与 LongAdder
| 场景 | 优先选择 | 原因 |
|---|---|---|
| 需要精确当前值并频繁读 | AtomicLong | 单一原子值,读取简单准确 |
| 高并发只做累加、统计时再汇总 | LongAdder | 分散到多个 Cell,降低 CAS 冲突 |
| 余额、库存等业务状态 | 不直接依赖 JVM 原子类 | 多实例下必须回到数据库/Redis 权威原子操作 |
LongAdder 的 sum() 汇总各 Cell,统计瞬间可能不是严格线性一致的,适合 QPS、指标计数,不适合支付金额。
ConcurrentHashMap
ConcurrentHashMap 不是“给 HashMap 加了一把大锁”。现代 JDK 使用数组桶、CAS、桶级同步和红黑树等机制:不同桶可并发更新;单个桶冲突时才协调。它不允许 null key/value,因为并发场景中 get(key) == null 无法区分“不存在”和“值为 null”。
1 | ConcurrentHashMap<String, CompletableFuture<Product>> loading = new ConcurrentHashMap<>(); |
常见错误是先 get 判断再 put,这仍可能竞态。需要原子“缺失则计算”时使用 computeIfAbsent,但映射函数应短小、不能递归修改同一 Map,也不要在里面做慢 HTTP/数据库调用。
其他容器怎么选
| 容器 | 适合场景 | 不适合场景 |
|---|---|---|
| ConcurrentHashMap | 共享 KV、并发读写 | 需要全局事务或长计算函数 |
| CopyOnWriteArrayList | 读远大于写、监听器列表 | 写频繁或列表很大,每次写都会复制数组 |
| ConcurrentLinkedQueue | 无界、非阻塞队列 | 需要背压和容量控制 |
| ArrayBlockingQueue | 有界生产消费、希望固定内存 | 需要非常大的弹性队列 |
| LinkedBlockingQueue | 队列任务处理 | 默认容量很大时容易隐藏积压,应显式设上限 |
| DelayQueue | 延迟到期任务 | 需要分布式可靠调度 |
生产场景:本地单飞加载
热点缓存失效时,同一实例的多个请求同时回源会造成击穿。可用 ConcurrentHashMap<Key, CompletableFuture<Value>> 合并同 Key 加载,但一定要在完成后清理条目、设置超时并限制下游并发。跨实例仍需要 Redis、本地缓存预热或限流配合。
高频面试题
| 问题 | 回答要点 |
|---|---|
| CAS 一定比锁快吗? | 低冲突常更好,高冲突自旋会耗 CPU;按竞争程度选择。 |
| LongAdder 为什么吞吐高? | 多个 Cell 分散竞争,累加时少争抢同一内存位置。 |
| ConcurrentHashMap 为什么不能存 null? | 并发 get 的 null 无法区分缺失与 value 为 null。 |
| COW 为什么适合监听器? | 读无锁且稳定,写少时复制成本可接受。 |


