concurrenthashmap通过分层协同机制实现高并发:读操作依赖volatile和unsafe无锁可见;写操作按桶状态动态选择cas插入、协助扩容或synchronized锁头节点;扩容由多线程协作完成,size统计采用分段计数。

要真正理解 ConcurrentHashMap 的并发控制,不能只记“CAS + synchronized”这六个字,得看它在什么时机用什么手段、为什么这么选、又如何协同运作。核心在于:**不同操作场景触发不同同步机制,且层层降级或升级,兼顾性能与安全**。
读操作为何几乎无锁?靠 volatile + UNSAFE 保证可见性
get 操作全程不加锁,但结果依然可靠,关键在两个设计:
- Node 字段全用 volatile 修饰:key、hash 是 final,val 和 next 明确声明为 volatile,确保写入后对其他线程立即可见;
- 底层用 UNSAFE.getObjectVolatile() 读取桶内节点:绕过普通引用读取的重排序风险,配合 LoadLoad + LoadStore 屏障,保证读到的是最新结构状态。
这意味着:即使另一个线程正在扩容、迁移某个桶,get 仍能安全读到旧表中的值,或跳转到新表——不会读到半截迁移的中间态。
写操作分四层判断:从无锁到轻量锁再到协作
put 过程不是统一上锁,而是按桶状态动态决策:
- 桶为空(null)→ 尝试 CAS 插入:最轻量,成功即返回,失败说明有竞争,进入下一轮;
- 桶是 ForwardingNode(-1)→ 协助扩容:当前线程主动帮着把旧表数据迁移到新表,提升整体进度;
- 桶头节点存在 → 对头节点加 synchronized:仅锁住该链表/红黑树的头,不影响其他桶;
- 插入后检查树化/扩容 → 调用 addCount() 分段计数:用类似 LongAdder 的 CounterCell 数组累加,避免 size 竞争。
扩容不是独占行为,而是多线程可参与的协作流程
JDK 8 的扩容不再是单线程搬运,而是:
- 通过 sizeCtl = -N 标记扩容中,其中 N 表示已有 (N−1) 个线程参与;
- 每个线程领取一个“步进区间”(默认 stride=16),只负责迁移 table 中连续的一段;
- 迁移时新建 ForwardingNode 占位,并把原桶设为该节点,后续 get/put 遇到它就自动跳转或协助;
- 所有线程完成各自区间后,再统一检查是否全部迁移完毕,更新 table 引用。
这种设计让扩容吞吐随 CPU 核心数线性增长,而不是卡在单点瓶颈。
为什么用 synchronized 而不用 ReentrantLock?源码里藏着性能权衡
很多人疑惑:既然都锁头节点了,为何不统一用 Lock?答案藏在 JVM 优化里:
- 锁消除(Lock Elision):JIT 编译器发现 synchronized 块作用域极小、无逃逸,会直接去掉锁开销;
- 偏向锁 & 轻量锁快速路径:在低竞争下,synchronized 进入轻量锁只需几条原子指令,比 ReentrantLock 的 AQS 队列管理更轻;
- 内存占用更少:synchronized 是 JVM 内置锁,对象头里存状态;ReentrantLock 需额外对象实例和 AQS Node 链表。
源码中所有 synchronized(this) 实际都作用于单个 Node 实例,正是为了匹配这种细粒度、短临界区的场景。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











