Skip to content

ConcurrentHashMap 原理、Segment 分段锁与 CAS+synchronized 演进 ​

回到总览:并发与运行时底座
相关模块:synchronized / Lock / volatile · Atomic* / CAS / LongAdder

一句话定义 ​

ConcurrentHashMap 是 Java 并发包(JUC)中高性能的并发哈希表;从 JDK 7 的 Segment 分段锁演进到 JDK 8 的 Node 数组 + CAS + synchronized 细粒度锁,实现了极高并发读写吞吐与线程安全保证。

代码索引 ​

对应 Lab:map-concurrency-compare · MapConcurrencyCompare.java

为什么需要 ​

  • 为什么 Hashtable 或 Collections.synchronizedMap() 在高并发场景下性能极差,必须替换为 ConcurrentHashMap?
    • 一句话答:Hashtable 使用一把大锁锁住整个方法与整个数组,导致所有线程无论读写哪个 Bucket 都在竞争同一把锁;而 ConcurrentHashMap 只锁住当前插入的 Bucket 头节点(Node),不同 Bucket 之间的写入完全并行互不干扰。
  • 为什么 10 年 Android 工程师必须了解 JDK 8 的 ConcurrentHashMap 架构演进?
    • 一句话答:JDK 8 废弃了重量的 Segment 继承体系,改用无锁 CAS 处理空槽插入,仅在发生哈希冲突时对链表/红黑树首节点加轻量级 synchronized 锁;这种“无锁 + 细粒度锁”思想是高并发优化的典范。

底层机制 ​

1. JDK 7 vs JDK 8 实现架构对比 ​

text
JDK 7: Segment 分段锁 (ReentrantLock 数组)
[Segment 0 (Lock)] ───► [HashEntry 数组]
[Segment 1 (Lock)] ───► [HashEntry 数组]

JDK 8: Node 数组 + CAS + synchronized 桶锁
[Node 0] ───► null                        (CAS 尝试无锁插入!)
[Node 1] ───► Node ───► Node             (synchronized 锁住 Node 1 节点)
[Node 2] ───► TreeNode (红黑树)           (synchronized 锁住 TreeNode 根节点)

两代设计对比表 ​

维度JDK 7 ConcurrentHashMapJDK 8 ConcurrentHashMap
加锁粒度Segment 分段锁 (默认 16 个 Segment)桶首节点 (Node / TreeNode)
锁类型ReentrantLockCAS + synchronized
数据结构Segment 数组 + HashEntry 数组 + 链表Node 数组 + 链表 + 红黑树
并发度依赖 Segment 数量 (默认 16)取决于 Bucket 数组大小 (可达几千)

2. 读操作 get() 完全无锁机制 ​

ConcurrentHashMap 的 get() 操作全程不需要加锁:

  1. Node 节点的 val 与 next 指针均声明为了 volatile,保证了多线程写入的可见性。
  2. 读线程通过 volatile 读可以直接拿到最新的节点与值,无需任何锁开销。

Android / Flutter / Web / Backend 对照 ​

维度Java / Android (JUC)Dart / FlutterGo 语言
并发 Map 方案ConcurrentHashMap无需锁 (单线程 Isolate 天然安全)sync.Map (读写分离/读无锁)
读写锁粒度桶头节点级锁无锁read 读 map (原子) + dirty 写 map (Mutex)

常见场景 ​

1. 使用 putIfAbsent 或 computeIfAbsent 保证复合操作原子性 ​

java
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();

// 错误示范:get 和 put 组合并非原子操作!
// if (!map.containsKey(key)) map.put(key, new AtomicInteger(1));

// 正确示范:使用 computeIfAbsent 保证线程安全的单次初始化
map.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();

常见误配、事故后果与排障 ​

1. 事故:误以为 ConcurrentHashMap.get() + put() 组合也是线程安全的 ​

  • 误配原因:代码中写 if (map.get(key) == null) { map.put(key, val); }。
  • 后果:虽然 get() 和 put() 单个方法是线程安全的,但“先查后写”两步之间存在竞态条件(Race Condition),导致多线程同时通过 get() == null 校验造成重复覆写!
  • 排障与修法:使用 putIfAbsent() 或 compute() 复合原子方法。

对应实验 ​

各章节内已嵌入跳转链接;此处汇总全部 Lab:

Lab说明源码
map-concurrency-compareHashMap / Hashtable / ConcurrentHashMap 并发读写与扩容行为对照MapConcurrencyCompare.java

复习检查题 ​

  1. JDK 8 的 ConcurrentHashMap 是如何做到读操作 get() 完全不需要加锁的?

    答:因为 ConcurrentHashMap 的 Node 节点的属性 val 和指向下一个节点的指针 next 均被 volatile 关键字修饰。volatile 语义保障了多线程间内存的实时可见性与有序性。当写线程在某一节点上追加新节点或修改值时,读线程能立刻通过 volatile 读获取最新数据,因此 get() 全程无需加锁。

  2. 为什么 JDK 8 放弃了分段锁 (Segment),改用 synchronized 锁住桶首节点?

    答:因为 Segment 分段锁的并发度受限于 Segment 数量(默认 16),且 Segment 继承自 ReentrantLock 带来了较重的内存开销。而锁住单个 Bucket 的首节点将锁粒度缩小到了最小,且随着 JDK 6 之后 synchronized 获得了偏向锁、轻量级锁和自旋锁等一系列优化,其性能在低中度竞争下极其优异,搭配 CAS 操作能最大化并发吞吐。

速记 ​

  • JDK 8 演进:放弃 Segment 分段锁,改用 Node 数组 + CAS + synchronized 桶首锁。
  • 读无锁写细粒:get() 靠 volatile 读实现完全无锁,写只锁冲突桶的首节点。
  • 防竞态条件:单独方法安全不等于组合安全,复合操作必用 computeIfAbsent。

站点构建时间:2026/8/24 23:43:17