为什么 ConcurrentHashMap 采用 synchronized 加锁而不采用ReentrantLock

文章目录

    • [1. 锁对象的演进:从"大锁"到"小锁"](#1. 锁对象的演进:从“大锁”到“小锁”)
    • [2. 为什么不全用原子操作(CAS)?](#2. 为什么不全用原子操作(CAS)?)
    • [3. 为什么选 `synchronized` 而不是 `ReentrantLock`?](#3. 为什么选 synchronized 而不是 ReentrantLock?)
      • [① 内置锁的"主场优势" (JVM 优化)](#① 内置锁的“主场优势” (JVM 优化))
      • [② 内存开销的极致压榨](#② 内存开销的极致压榨)
    • [4. 总结:现在的协作逻辑](#4. 总结:现在的协作逻辑)
    • 核心结论

这是一个快手的面试题,当时我被问到了
jdk1.7采用了segment+ReentranceLock
jdk1.8采用CAS+synchronized处理竞态
这是一个非常经典且能体现 Java 性能演进的问题。

其实 ConcurrentHashMap 并不是完全放弃了原子操作。准确地说,它现在的设计策略是:先用原子操作(CAS),万不得已才动用 synchronized

在 Java 8 之后,它的这种转变核心逻辑是为了在极细粒度 的锁竞争下,压榨出比 ReentrantLock 更高的性能。我们可以从以下几个维度来系统分析:


1. 锁对象的演进:从"大锁"到"小锁"

  • Java 7(Segment + ReentrantLock)
    它把 Map 分成了 16 个 Segment,每个 Segment 都是一个 ReentrantLock。这意味着如果你有两个不同的 Key 落在同一个 Segment 里,哪怕它们在不同的桶(Bucket)上,也得竞争同一把锁。
  • Java 8+(Node + synchronized)
    它取消了 Segment,直接锁住每个桶的第一个节点(Node)
    • CAS 负责"无中生有" :如果桶是空的,直接通过 CAS 插入,完全不加锁。
    • synchronized 负责"精细管理" :只有当桶里已经有数据(发生碰撞)时,才针对这一个桶的头节点加 synchronized

2. 为什么不全用原子操作(CAS)?

原子操作(如 compareAndSet)虽然快,但它只能保证单个变量修改的原子性。

  • 痛点:当发生哈希冲突时,我们需要在链表或红黑树上进行一系列复杂操作(比如遍历、比较、插入、平衡旋转)。
  • 局限性:要把这一整套"组合拳"全部封装进一个 CAS 里,逻辑会变得极其复杂且难以维护,甚至会因为频繁的自旋(Spinning)导致 CPU 空转,浪费大量资源。此时,使用锁来保护整个代码块是更稳妥、更高效的选择。

3. 为什么选 synchronized 而不是 ReentrantLock

这是很多人的误区,认为 synchronized 慢。但在 Java 8 及以后的版本中,情况发生了反转:

① 内置锁的"主场优势" (JVM 优化)

synchronized 是 JVM 的亲儿子,拥有各种"黑科技"加持:

  • 锁升级机制 :它会经历 偏向锁 -> 轻量级锁 -> 重量级锁 的动态演进。在竞争不激烈时,它的开销极低。
  • 锁消除与锁粗化 :JIT 编译器能根据代码运行情况,自动把没必要的锁删掉,或者把多个小锁合并。这些是作为第三方类库的 ReentrantLock 很难做到的。

② 内存开销的极致压榨

  • ReentrantLock 是一个对象,它需要维护 AQS 同步队列、锁状态等一系列成员变量。
  • 如果 Map 有 1024 个桶,全用 ReentrantLock 就要创建 1024 个锁对象,这非常浪费内存。
  • synchronized 直接利用对象头(Mark Word)里的位来标记锁状态,不需要额外创建锁对象 。对于 ConcurrentHashMap 这种动辄成千上万个桶的结构来说,内存节省非常可观。

最好看一下下面这三篇我写的文章,可以更好的理解
为什么不逃逸代表不需要锁,JIT会直接删掉锁
什么是逃逸分析
锁消除和锁粗化

4. 总结:现在的协作逻辑

ConcurrentHashMap 的源码里,一次 put 操作的逻辑是这样的:

  1. 计算哈希值
  2. 如果是空桶 :尝试用 CAS 插入。成功则返回。
  3. 如果正在扩容 :当前线程去协助搬迁(Help Transfer)。
  4. 如果是旧桶冲突 :使用 synchronized 锁住头节点,进行链表或红黑树的操作。

核心结论

ConcurrentHashMap 采用 synchronized 的核心痛点不是为了代替原子操作,而是为了在原子操作覆盖不到的复杂逻辑区(链表/树操作),提供一种内存更省、底层优化更强、且粒度更细的同步机制。

现在的 synchronized 已经不是当年那个笨重的"重量级锁"了,它在低竞争场景下的性能表现,已经足以让它在集合框架中重新坐稳"一哥"的位置。

相关推荐
二哈赛车手4 小时前
新人笔记---idea索引失效问题解决方案
java·笔记·spring·elasticsearch·intellij-idea
飞天狗1114 小时前
零基础JavaWeb入门——第五课第一小节:九大内置对象 · 第1个:request(请求对象)
java·开发语言·前端·后端·servlet
a15108416934 小时前
记一次大模型探索
java·服务器·前端
c++之路4 小时前
Bazel C++ 构建系列文档(五):多目标与多包项目
java·开发语言·c++
云烟成雨TD4 小时前
Agent Scope Java 2.x 系列【11】中间件(Middleware):核心设计
java·人工智能·agent
心之伊始4 小时前
Spring AI Chat Memory 实战:用 JDBC 给 Java Agent 加会话记忆
java·spring boot·agent·spring ai·chat memory
凡人叶枫5 小时前
Effective C++ 条款40:明智而审慎地使用多重继承
java·数据库·c++·嵌入式开发·effective c++
放弃 治疗5 小时前
宝塔面板安装 JDK 完整教程|Java 环境配置详解
java·开发语言
至此流年莫相忘5 小时前
Spring 依赖注入三剑客:@Autowired、@Resource 与 @RequiredArgsConstructor 深度对比与实战指南
java·数据库·spring
零陵上将军_xdr5 小时前
为什么DCL单例要加volatile?——CPU乱序执行与内存屏障
java·linux