concurrenthashmap的“分段锁”仅存在于jdk 1.7,jdk 1.8及以后版本已彻底移除segment,改用桶级synchronized锁与cas协作机制;jdk 1.7通过segment数组实现分段加锁,每个segment继承reentrantlock并管理独立hashentry表;jdk 1.8则以node[] table为核心,通过volatile字段、cas操作和头节点加锁实现更细粒度并发控制。

ConcurrentHashMap 的“分段锁”只存在于 JDK 1.7,JDK 1.8 及以后版本已彻底移除 Segment,改用更细粒度的桶级锁 + CAS 协作机制。剖析源码时,必须先明确版本,否则容易混淆概念、得出错误结论。
JDK 1.7:从 Segment 数组切入,看分段锁如何落地
在 ConcurrentHashMap.java(JDK 1.7)中,核心结构是:
-
Segment
[] segments :默认长度为 16 的数组,每个 Segment 继承ReentrantLock,本身就是一个可重入锁; -
HashEntry
[] table :每个 Segment 内部维护独立的哈希表,操作前先通过hash 定位到具体 Segment; -
put 操作流程:定位 Segment → 尝试获取该 Segment 的锁(
lock())→ 在其内部 table 中执行插入 → 释放锁; - 扩容是逐段进行的,一个 Segment 扩容不影响其他 Segment,但整体扩容耗时长、内存开销大。
JDK 1.8:聚焦 Node 数组与 volatile + CAS 的协作逻辑
1.8 版本不再有 Segment 类,源码入口是 ConcurrentHashMap.java 中的 Node<k>[] table</k> 和关键字段:
- sizeCtl:volatile int,作为状态控制器——值为 -1 表示正在初始化,为负数(如 -N)表示有 N-1 个线程正在协助扩容;
-
initTable():多个线程竞争初始化时,靠
U.compareAndSwapInt(this, SIZECTL, sc, -1)保证仅一个线程真正建表; -
putVal():先尝试 CAS 插入空桶;若桶非空,则以头节点为锁对象,用
synchronized加锁处理链表或红黑树; -
helpTransfer():当发现当前桶是
ForwardingNode(扩容标记),会主动协助迁移数据,体现“扩容可并行”的设计。
读操作为什么无锁?关键在 volatile 与 final 的语义保障
ConcurrentHashMap 的 get 操作全程不加锁,依赖的是:
- Node 的 val 和 next 字段声明为 volatile:确保读取时能看到其他线程写入的最新值;
- table 数组引用为 volatile:保证 table 初始化完成后的可见性;
- Node 构造时 key/value 为 final:避免构造未完成就被其他线程读到半初始化对象;
- 读操作可能看到“弱一致性”结果(比如刚 put 完还没来得及更新 size),但不会出现数据错乱或崩溃。
调试建议:用断点+日志验证锁行为和 CAS 失败路径
想真正看清运行时行为,不能只读代码,还要动手验证:
- 在 JDK 1.7 的
Segment.lock()和unlock()处打断点,观察多线程是否真的只锁住对应段; - 在 JDK 1.8 的
U.compareAndSwapInt调用后加日志,模拟高并发下 CAS 失败再重试的典型路径; - 触发扩容(如 put 超过阈值),观察
sizeCtl如何从正数变为负数,再逐步归零; - 用 JOL(Java Object Layout)工具查看
Node对象内存布局,理解为何 volatile 修饰 next 能保障链表遍历安全。











