面试记录 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 锁升级(偏向锁→轻量级锁→重量级锁),性能追平甚至超过 ReentrantLock
- 说对语义简单性:synchronized 自动加解锁 vs ReentrantLock 手动 lock()/unlock()(try-finally 防漏)
- 提到 JVM 更容易优化 synchronized(内建语法级支持)
- 逻辑清晰,主线正确
不足:
- 表述小瑕疵:"JDK1.6 之前新的 synchronized 一直是加全量锁"——应为"重量级锁"(重量级锁 vs 全量锁是不同概念)
- 未提 JDK7 为什么用 ReentrantLock:这是关键上下文——JDK7 的 Segment 需要 tryLock(非阻塞尝试加锁)+ lockInterruptibly(可中断)+ 公平锁等 ReentrantLock 独有特性;JDK8 桶级锁语义简单不需要这些特性才切回 synchronized
- 未提锁粒度匹配:JDK8 桶级锁竞争时间极短,synchronized 的偏向锁/轻量级锁快速路径正好匹配;换成 ReentrantLock 反而多了对象分配和状态机维护开销
- 未提 AQS 复杂度:ReentrantLock 基于 AQS(volatile int state + CLH 队列),有对象头和内存占用;synchronized 用 Mark Word 内联存储锁状态,内存开销更小
- 未提 JIT 优化友好:JIT 编译器对 synchronized 有专门优化(逃逸分析后直接消除锁、锁消除);ReentrantLock 是库级实现 JIT 无法穿透
- 笔误:"GM"应为"JVM/Java 团队"
补充(完整决策树):
- 技术演进:JDK5/6 前 synchronized 只有重量级锁;JDK6 引入锁升级,性能追平 ReentrantLock;JDK7 仍用 ReentrantLock 因为需要 tryLock/公平锁/可中断;JDK8 桶级锁语义简单,切回 synchronized
- 场景匹配:桶级锁竞争极短(微秒级),偏向锁/轻量级锁快速路径正好命中
- 实现简洁性:synchronized 是 JVM 内建语法,无对象分配、无状态机、无手动 unlock 遗漏风险
- JIT 优化:synchronized 可被 JIT 逃逸分析消除、锁粗化;ReentrantLock 是库实现无法穿透
- 内存占用:synchronized 用 Mark Word 内联存储;ReentrantLock 要额外分配对象+同步队列
完整讲解
第一部分:知识重点剖析
核心考点定位:验证是否理解 JDK6 synchronized 演进的技术决策,以及"什么场景选什么锁"的权衡思维。
知识点拆解:
- 底层原理:JDK5 及以前 synchronized 只有重量级锁(OS 内核 mutex,用户态到内核态切换 10μs+),性能比 ReentrantLock 差 10 倍。JDK6 引入锁升级机制:无锁(Mark Word 存储 hashCode)→ 偏向锁(无竞争时 CPU 直接标记线程 ID,零开销)→ 轻量级锁(CAS 尝试加锁,无自旋升级)→ 重量级锁(真正的 mutex)。锁升级基于竞争激烈程度自动迁移,避免了重量级锁的一次性成本。
- ReentrantLock 的代价:基于 AQS(AbstractQueuedSynchronizer),需要 volatile int state + CLH 队列维护;每次加锁要 CAS state、失败则入队 park、唤醒后 unpark。每次操作都要读写共享变量,缓存行乒乓开销大。
- 设计权衡:
- 性能:JDK6+ synchronized 在无竞争/低竞争场景比 ReentrantLock 快;竞争激烈时二者相当
- 内存:synchronized 用 Mark Word 内联存储,0 额外对象;ReentrantLock 要 new 对象 + AQS state + 队列
- 语义:synchronized 自动解锁(异常也解锁),ReentrantLock 必须手动 unlock,try-finally 包起来防漏
- JIT 优化:synchronized 是内建指令,JIT 可做逃逸分析消除锁、锁粗化、锁消除;ReentrantLock 是库调用 JIT 无法穿透
- JDK7 为什么用 ReentrantLock:Segment 需要
tryLock 非阻塞尝试(避免死等)、lockInterruptibly 可中断、公平锁选项。这些是 ReentrantLock 独有特性。JDK8 桶级锁粒度更细、竞争更短,不需要这些高级特性,切回 synchronized 更合适。
- 工程实践:现代代码默认用 synchronized(简洁);需要可中断/tryLock/超时/条件变量时用 ReentrantLock;读多写少用 ReadWriteLock;无锁场景用 CAS/AtomicXxx。
按「底层原理 → 设计权衡 → 工程实践」递进排列。
第二部分:考察方向与延伸递进
考察方向:
- 深度:能否画出 JDK6 synchronized 锁升级路径,说清每一步的开销差异
- 广度:能否对比 synchronized、ReentrantLock、ReadWriteLock、StampedLock、CLH 队列的选型
- 应用:能否根据具体锁竞争特征(竞争持续时间、竞争次数)选择合适锁
延伸递进面试题:
- synchronized 锁升级的四种状态?
- 解答要点:无锁(Mark Word 存 hashCode)→ 偏向锁(存线程 ID,无竞争 CPU 直返)→ 轻量级锁(CAS 尝试,失败自旋最多 10 次)→ 重量级锁(真实 mutex,线程 park)。升级单向不可逆。
- 偏向锁在 JDK15 后被默认禁用?
- 解答要点:JDK15 JEP 374 默认禁用偏向锁。原因:偏向锁撤销成本高(全局 safepoint + 重偏向),现代 JIT 优化已能更好地处理无竞争锁。可通过
-XX:-UseBiasedLocking 手动禁用。
- 什么时候必须用 ReentrantLock 而不是 synchronized?
- 解答要点:需要
tryLock() 非阻塞加锁(如避免死等);需要 lockInterruptibly() 可中断;需要公平锁;需要多个 Condition 条件变量;需要 lock(long, TimeUnit) 超时加锁。
- 为什么 ConcurrentHashMap 桶级锁不需要 ReentrantLock 的高级特性?
- 解答要点:桶级锁竞争极短(微秒级),无阻塞死等风险;不需要可中断(迁移是短任务);不需要公平锁(吞吐优先);单个桶一个条件就够了。synchronized 的简洁性正好匹配。
第三部分:面试者参考回答(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。