← 每日面试题 学习看板

ConcurrentHashMap为什么在JDK 1.8中使用`synchronized`而不是`ReentrantLock`

面试题/00java/01Java 基础/05 集合/ConcurrentHashMap为什么在JDK 1.8中使用`synchronized`而不是`ReentrantLock`.md

面试记录 2026-10-09

题目:ConcurrentHashMap 为什么在 JDK 1.8 中使用 synchronized 而不是 ReentrantLock?

难度:high

标签:java/collections, general/knowledge

评分:⭐⭐⭐⭐☆(4/5)

我的回答

ConcurrentHashMap 在 JDK1.8 中使用新的 synchronized 而不是 ReentrantLock,核心原因是在 JDK1.6 的时候,新的 synchronized 被 JVM 进行了一次升级,升级后它的性能跟效果其实是相对来说不输于 ReentrantLock 的,甚至比它更好。而有这个说法,为什么不用它?原因在于 JDK1.6 之前的版本使用的新的 synchronized,它的性能相对很低,它一直是加全量的锁。重量级锁,导致它性能比较低。但是在 JDK1.6 之后,它开始使用锁升级的一系列操作,包括从无锁到偏向锁到轻量级锁到重量级锁这一套升级过程,使得它本身的性能在不同的环境下有不同的选择,使得性能有一定一定程度提高。对比 ReentrantLock 来说,它的性能和使用方法更简单。而 ReentrantLock 它的使用,首先你需要创建锁对象,然后还需要进行人工手动的加锁跟解锁,它使用上在操作上会更繁琐。综合语义、内核和自身的考虑,GM 最终在 ConcurrentHashMap 中使用 synchronized 作为自身的锁,进行锁处理。

点评

优点:

不足:

补充(完整决策树):

完整讲解

第一部分:知识重点剖析

核心考点定位:验证是否理解 JDK6 synchronized 演进的技术决策,以及"什么场景选什么锁"的权衡思维。

知识点拆解:

  1. 底层原理:JDK5 及以前 synchronized 只有重量级锁(OS 内核 mutex,用户态到内核态切换 10μs+),性能比 ReentrantLock 差 10 倍。JDK6 引入锁升级机制:无锁(Mark Word 存储 hashCode)→ 偏向锁(无竞争时 CPU 直接标记线程 ID,零开销)→ 轻量级锁(CAS 尝试加锁,无自旋升级)→ 重量级锁(真正的 mutex)。锁升级基于竞争激烈程度自动迁移,避免了重量级锁的一次性成本。
  2. ReentrantLock 的代价:基于 AQS(AbstractQueuedSynchronizer),需要 volatile int state + CLH 队列维护;每次加锁要 CAS state、失败则入队 park、唤醒后 unpark。每次操作都要读写共享变量,缓存行乒乓开销大。
  3. 设计权衡:
  1. JDK7 为什么用 ReentrantLock:Segment 需要 tryLock 非阻塞尝试(避免死等)、lockInterruptibly 可中断、公平锁选项。这些是 ReentrantLock 独有特性。JDK8 桶级锁粒度更细、竞争更短,不需要这些高级特性,切回 synchronized 更合适。
  2. 工程实践:现代代码默认用 synchronized(简洁);需要可中断/tryLock/超时/条件变量时用 ReentrantLock;读多写少用 ReadWriteLock;无锁场景用 CAS/AtomicXxx。
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. synchronized 锁升级的四种状态?
  1. 偏向锁在 JDK15 后被默认禁用?
  1. 什么时候必须用 ReentrantLock 而不是 synchronized?
  1. 为什么 ConcurrentHashMap 桶级锁不需要 ReentrantLock 的高级特性?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,JDK8 ConcurrentHashMap 从 JDK7 的 ReentrantLock 切回 synchronized,核心是 JDK6 之后 synchronized 的演进让它在性能和语义上都更优。

JDK6 之前,synchronized 只有重量级锁,走的是操作系统 mutex,用户态到内核态切换开销非常大,性能比 ReentrantLock 差 10 倍左右。所以 JDK7 的 ConcurrentHashMap 只能用 ReentrantLock。

JDK6 之后,JVM 对 synchronized 做了大幅优化——锁升级机制:一开始是无锁状态,Mark Word 里存 hashCode;第一次加锁进偏向锁,Mark Word 直接写入线程 ID,无竞争时 CPU 直接返回,几乎零开销;第二次被其他线程竞争转轻量级锁,用 CAS 尝试,失败则自旋;再竞争升级到重量级锁,才走真正的 OS mutex。整个升级路径基于竞争激烈程度自动迁移。同时 JIT 编译器对 synchronized 有专门优化:逃逸分析可以消除不必要的锁、锁粗化可以合并相邻小锁、锁消除可以干掉死锁代码。

对比 ReentrantLock,JDK8 的场景下 synchronized 有四个优势。第一,性能——JDK6 之后无竞争和低竞争场景 synchronized 更快,因为 ReentrantLock 每次加锁都要 CAS volatile state、维护 CLH 队列,缓存行乒乓开销更大。第二,内存——synchronized 用 Mark Word 内联存储锁状态,0 额外对象;ReentrantLock 要 new 一个对象加 AQS state 加队列,内存占用明显。第三,语义——synchronized 是 JVM 内建语法,自动加解锁,即使抛异常也会解锁;ReentrantLock 必须手动 lock/unlock,还要 try-finally 包起来防漏。第四,JIT 友好——synchronized 可以被 JIT 逃逸分析消除、锁粗化,ReentrantLock 是库调用 JIT 无法穿透。

那为什么 JDK7 用 ReentrantLock?因为 Segment 需要 tryLock() 非阻塞尝试加锁、lockInterruptibly() 可中断、公平锁这些高级特性,都是 ReentrantLock 独有的。JDK8 桶级锁粒度细到单个桶,竞争极短、无死等风险、无中断需求,synchronized 的简洁性正好匹配。

所以 JDK8 的这次切换,本质是"JVM 技术演进 + 场景简化"的必然结果。现代代码里默认用 synchronized,只在需要 tryLock、可中断、公平锁、多个 Condition 时才用 ReentrantLock。