synchronized 与锁机制

共 23 题
📑 题目列表 23 题
#
★★★

1. JEP 491 在 JDK 24 解决 synchronized pin 的方法

JEP 491 在 JDK 24 是如何解决 synchronized 钉住(pin)问题的?

  • 钉住问题
  • JEP 491 的方法
  • 虚拟线程阻塞

JEP 491(JDK 24)解决虚拟线程在 synchronized 块内阻塞时钉住载体线程的问题。方法:当虚拟线程持有 synchronized 监视器并阻塞时,JVM 在阻塞点释放监视器,让载体线程卸载去执行其他虚拟线程;当虚拟线程被唤醒后,重新竞争并获取监视器,再继续执行。通过"阻塞时释放、恢复时重获"的机制,虚拟线程在 synchronized 内阻塞不再占用载体线程,解除了钉住。JEP 491 使 synchronized 在虚拟线程下与 ReentrantLock 一样可卸载。

钉住源于"监视器与载体线程绑定"。JEP 491 打破该绑定,让虚拟线程阻塞时释放监视器、恢复时重获,从而实现卸载。

#
★★★

2. Java 25 虚拟线程下 synchronized 性能优化结论

Java 25 虚拟线程下 synchronized 的性能优化结论是什么?

  • JEP 491 效果
  • synchronized 与 ReentrantLock 对比
  • 性能结论

Java 25 中,JEP 491 已消除虚拟线程下 synchronized 的钉住问题,synchronized 作为内置锁在虚拟线程下不再占用载体线程,性能与 ReentrantLock 相当甚至更优(synchronized 由 JVM 直接支持、可被 JIT 优化如锁消除/锁粗化)。因此虚拟线程下默认使用 synchronized 即可获得良好性能,只有在需要可中断、超时、公平、多 Condition 等高级能力时,才用 ReentrantLock。结论:JEP 491 后 synchronized 在虚拟线程下的性能劣势已消除,选型回归功能需求。

JEP 491 让 synchronized 在虚拟线程下"解锁",性能不再是选型障碍,synchronized 成为虚拟线程的默认锁。

#
★★★

3. Java 对象头(Mark Word)与锁状态(无锁/偏向/轻量/重量)

Java 对象头(Mark Word)与锁状态(无锁/偏向/轻量/重量)的关系是什么?

  • Mark Word 结构
  • 锁状态编码
  • 状态切换

Java 对象头包含 Mark Word(通常 64 位),Mark Word 的位模式编码了锁状态:无锁(hashcode、age 等)、偏向锁(偏向线程 ID)、轻量级锁(指向栈中锁记录)、重量级锁(指向 Monitor 指针)。JVM 根据竞争情况在 Mark Word 中切换状态位:无锁→偏向→轻量→重量,随竞争升级。Mark Word 是锁状态与元信息的复用载体,不同状态用不同位布局。

Mark Word 是锁机制的核心。锁状态由 Mark Word 的位模式决定,理解它能解释锁升级与撤销。

#
★★★

4. Thread.holdsLock 与死锁探测

Thread.holdsLock 与死锁探测有什么关系?

  • holdsLock 语义
  • 死锁探测
  • 使用场景

Thread.holdsLock(obj) 用于判断当前线程是否持有 obj 的监视器锁,常用于断言(如要求调用方持锁)或调试。死锁探测本身通常用 jstack/jcmd Thread.print 或 JMX 的 findDeadlockedThreads(依赖 JVM 的锁依赖图分析)。holdsLock 可用于代码内的辅助:在工具方法中校验"是否持锁"以保护不变量。死锁探测是运行时工具,holdsLock 是编程辅助,二者配合:holdsLock 做前置校验,JVM 工具做实际死锁检测。

holdsLock 定位"当前线程是否持锁",死锁探测定位"多个线程互相等待"。前者是编程断言,后者是诊断工具。

#
★★★

5. 偏向锁的撤销成本与 JDK 15 默认禁用原因

偏向锁的撤销成本是什么?JDK 15 为什么默认禁用偏向锁?

  • 偏向锁的机制
  • 撤销成本
  • JDK 15 禁用原因

偏向锁为"同一线程多次获取"优化,一次性把锁偏向到该线程,后续获取无需 CAS。但撤销成本高:当其他线程竞争时,需撤销偏向(STW 暂停、遍历线程、恢复 Mark Word),频繁撤销比普通锁更慢。JDK 15 默认禁用偏向锁,原因是现代应用竞争通常不满足"单线程长时间占锁"的假设,偏向锁的撤销开销(尤其撤销风暴)超过收益,且与虚拟线程等新特性不兼容。禁用后默认走轻量锁/重量锁路径。

偏向锁优化"单线程"场景,但撤销成本高且收益场景少,JDK 15 判定不值得,默认关闭。

#
★★★

6. 轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)

轻量级锁与偏向锁分别是什么?

  • 偏向锁机制
  • 轻量级锁机制
  • 两者的区别

偏向锁:假设锁偏向单个线程,首次获取时把线程 ID 写入 Mark Word,之后该线程无需 CAS 即可进入,适合"单线程反复获取"的场景;但竞争时需撤销。轻量级锁:无竞争(或低竞争)时,用 CAS 把 Mark Word 改为指向栈中锁记录的指针,加锁/解锁走 CAS,无需阻塞;竞争加剧时升级为重量级锁。区别:偏向锁连 CAS 都省(同一线程直接进),轻量锁用 CAS 自旋,两者都是"无竞争优化",重量级锁才是阻塞。

两者都是低竞争优化,偏向锁省 CAS、轻量锁用 CAS。升级链:偏向→轻量→重量,随竞争加剧。

#
★★

7. synchronized 修饰实例方法、静态方法、代码块的锁对象差异

synchronized 修饰实例方法、静态方法、代码块的锁对象有哪些差异?

  • 实例方法锁 this
  • 静态方法锁 Class
  • 代码块锁任意对象

synchronized 实例方法:锁对象是当前实例(this),不同实例互不影响;静态方法:锁对象是该类的 Class 对象,同类静态方法互斥;代码块:锁对象是显式指定的任意对象(synchronized(obj)),灵活控制范围。三者锁的粒度不同:实例方法锁实例、静态方法锁类、代码块锁指定对象。务必注意:若想保护同一份数据,所有线程必须用同一把锁。

锁对象决定了互斥范围。实例方法用 this,静态方法用 Class,代码块任意指定。选错锁对象会导致同步失效。

#
★★

8. synchronized 关键字的底层实现(Monitor、ACC_SYNCHRONIZED)

synchronized 关键字的底层实现是什么(Monitor、ACC_SYNCHRONIZED)?

  • 字节码层面
  • Monitor
  • 方法/块差异

字节码层面:synchronized 方法在访问标志中加 ACC_SYNCHRONIZED,JVM 隐式获取/释放监视器;synchronized 代码块编译为 monitorenter/monitorexit 指令(配异常处理保证释放)。底层是 Monitor 机制:每个对象关联一个 Monitor(ObjectMonitor),Monitor 记录持有者、重入计数、等待队列。进入时获取 Monitor(锁升级链),退出时释放。Monitor 是 synchronized 的同步核心,配合锁升级(偏向/轻量/重量)实现。

synchronized 底层 = 字节码指令(ACC_SYNCHRONIZED 或 monitorenter/exit)+ Monitor 机制 + 锁升级。Monitor 是本质。

#
★★

9. 对象头(Object Header)的 Mark Word 在不同锁状态下的位图布局

对象头(Object Header)的 Mark Word 在不同锁状态下的位图布局是什么?

  • 无锁布局
  • 偏向/轻量/重量布局
  • 位图分配

Mark Word(64 位)在不同锁状态下的位图布局:无锁:hashcode(31) + age(4) + biased_lock(1) + lock(2);偏向锁:偏向线程 ID(54) + epoch(2) + age(4) + biased=1 + lock(2);轻量级锁:指向栈中锁记录的指针(62) + lock(2);重量级锁:指向 Monitor 的指针(62) + lock(2)。lock 位(2 位)区分状态:00 轻量、01 无锁/偏向、10 重量、11 GC 标记。不同状态复用同一 64 位,按状态切换位布局。

Mark Word 的位图是"状态复用"设计,2 位 lock 标志区分四态,其余位按状态装载不同信息。

#
★★

10. 死锁检测工具(jstack/jcmd Thread.print)在生产环境的采样与分析策略

死锁检测工具(jstack/jcmd Thread.print)在生产环境的采样与分析策略是什么?

  • jstack/jcmd 用法
  • 多次采样
  • 分析策略

生产环境用 jstack pid 或 jcmd pid Thread.print 打印线程 dump,其中包含死锁检测(found one Java-level deadlock 部分)。采样策略:1) 多次采样(间隔 1-3 秒),避免单次 dump 的偶然性;2) 比对多个 dump 找出一致性(同一线程长期处于 BLOCKED/WAITING 且互相等待);3) 结合线程栈看锁的持有与等待链(holding lock / waiting to lock);4) 关联业务(线程名、调用栈)定位持锁点。注意生产环境 dump 会短暂 STW,需在低峰或谨慎执行。

死锁检测核心是"多次采样 + 锁依赖链分析"。单次 dump 可能误判,多次采样确认稳定等待关系。

#
★★

11. 死锁的产生条件与 jstack 定位死锁

死锁的产生条件是什么?如何用 jstack 定位死锁?

  • 死锁四条件
  • jstack 死锁检测
  • 定位步骤

死锁四条件:互斥、占有且等待、不可剥夺、循环等待。jstack 定位:执行 jstack pid,输出中若显示 "Found one Java-level deadlock" 即检测到;该部分列出死锁线程、各自持有的锁、等待的锁,形成循环依赖。定位步骤:1) 找到死锁线程(BLOCKED 状态);2) 看每个线程"waiting to lock"的锁与"holding"的锁;3) 确认循环等待链;4) 结合代码修锁顺序或加超时。工具是辅助,根本解决靠锁顺序与超时设计。

死锁定位靠"锁持有链 + 循环等待检测"。jstack 自动给出死锁循环,结合代码分析锁顺序。

#
★★

12. 活锁、饥饿与公平性三者在并发场景下的差异,如何识别并用锁策略解决?

活锁、饥饿与公平性三者在并发场景下的差异是什么?如何识别并用锁策略解决?

  • 活锁/饥饿/公平性定义
  • 三者的差异
  • 锁策略解决

活锁:线程都在运行但互相让路、不断重试,始终无进展(如两个线程互相让资源)。饥饿:某个线程长期得不到资源(如被高优先级或插队线程抢占锁)。公平性:锁是否按 FIFO 分配。差异:活锁是"活跃但无进展",饥饿是"某个线程无进展",公平性决定是否避免饥饿。解决:活锁用随机退避/错峰打破让步循环;饥饿用公平锁(FIFO,避免插队)或避免长时间持锁;公平性用 ReentrantLock(true) 或按需公平。

活锁、饥饿与公平性:三者都涉及"某个线程无法取得进展",但机制不同:活锁是互相干扰,饥饿是长期被抢占,公平性是分配策略。应用退避/公平锁区分。

#
★★

13. 自旋锁的适用场景与自适应自旋(JDK 6+)

自旋锁的适用场景是什么?自适应自旋(JDK 6+)是什么?

  • 自旋锁适用场景
  • 自适应自旋
  • 与阻塞对比

自旋锁适用场景:临界区极短、竞争低、期望持锁时间短,此时自旋等待(无上下文切换)比阻塞更高效。JDK 6+ 的自适应自旋:JVM 根据"上一个线程持锁时间"和"自旋成功的历史"动态调整自旋次数,上次自旋成功则本次多自旋,失败则少自旋,避免盲目自旋或过早阻塞。自适应自旋让 JVM 在"忙等"与"阻塞"间自适应,兼顾低延迟与低 CPU 占用,是 synchronized 锁升级(轻量→重量)中的关键优化。

自适应自旋是 JVM 对"自旋次数"的智能调谐。短临界区自旋划算,长临界区转阻塞,自适应找平衡点。

#
★★

14. 读写锁在读多写少与写饥饿场景下的表现如何评估,何时应改用 StampedLock 或公平策略?

读写锁在读多写少与写饥饿场景下的表现如何评估?何时应改用 StampedLock 或公平策略?

  • 读多写少表现
  • 写饥饿
  • 改用 StampedLock/公平策略

读多写少时,ReentrantReadWriteLock 读锁可共享,读吞吐高;但读多写少也可能导致写饥饿(大量读持续持有读锁,写锁拿不到)。评估:看读/写比例、写延迟要求、写饥饿程度。应对:1) 高读低写且读可重试时改 StampedLock 乐观读(读无锁,写更易获得);2) 用公平策略(公平读写锁)避免写饥饿,但牺牲吞吐;3) 限制读锁持有时间,或使用写优先策略。选型:读远多于写且读可重试→StampedLock;写延迟敏感→公平策略。

读写锁的弱点是写饥饿。StampedLock 乐观读让读不占锁,写更易获锁;公平策略保证写不饿死。

#
★★

15. 读写锁降级为何允许先取写锁再取读锁,反向升级又为什么容易形成互相等待

读写锁降级为何允许先取写锁再取读锁?反向升级为什么容易形成互相等待?

  • 写锁降级读锁
  • 读锁升级写锁
  • 死锁/等待

写锁降级(先写锁后读锁,再释放写锁)是安全的:写锁独占时再取读锁必然成功(无其他读者),释放写锁后仍持有读锁,保证连续一致的读视角,常用于缓存更新后保证读一致。反向升级(先读锁后写锁)危险:多个线程都持有读锁,都想升级为写锁,但写锁需要所有读锁释放,互相等待永远无法完成,形成死锁。因此 Java 只允许降级、禁止升级。

降级安全(独占时取读锁不冲突),升级死锁(多读者互相等写锁)。这是读写锁的设计约束。

#
★★

16. 锁消除(Lock Elision)与锁粗化(Lock Coarsening)

锁消除(Lock Elision)与锁粗化(Lock Coarsening)是什么?

  • 锁消除
  • 锁粗化
  • JIT 优化

锁消除:JIT 通过逃逸分析发现锁对象不逃逸(仅线程内使用),删除不必要的同步操作,避免加锁开销。锁粗化:JIT 发现相邻的多次加锁/解锁(如循环内反复 synchronized 同一对象)合并为一次较长临界区,减少加解锁次数。两者都是 JIT 的同步优化:锁消除删无用锁,锁粗化合并多锁。它们依赖逃逸分析与场景分析,是 synchronized 性能比 ReentrantLock 有优势的原因之一(ReentrantLock 不可被这些优化自动消除)。

锁消除靠逃逸分析"证伪逃逸",锁粗化靠"合并相邻临界区"。两者是 JIT 对 synchronized 的优化,ReentrantLock 无此待遇。

#
★★

17. 锁竞争(Lock Contention)监控与线程 dump 解读

锁竞争(Lock Contention)如何监控?线程 dump 如何解读?

  • 锁竞争监控
  • 线程 dump 解读
  • 定位竞争点

锁竞争监控:用 JFR 的 Monitor 相关事件(jdk.JavaMonitorEnter、锁等待)、JMX 的锁信息、线程 dump 分析。线程 dump 解读:看线程状态(BLOCKED 表示等待锁,WAITING 表示等待条件/join)、"locked" 与 "waiting to lock" 标记(表示已持有或等待某锁)、等待同一个锁的线程数(竞争程度)。定位竞争点:统计哪些对象被大量线程等待、持锁时间长短,结合 JFR 量化锁等待时长与次数,找出热点锁。

锁竞争监控 = 线程 dump 的"BLOCKED/waiting to lock" + JFR 锁事件量化。解读关键在于锁的持有与等待链。

#
★★

18. 锁粒度(粗/细)的取舍与场景

锁粒度(粗/细)的取舍与适用场景是什么?

  • 粗粒度锁
  • 细粒度锁
  • 取舍

粗粒度锁:用一个锁保护大范围数据,实现简单、竞争集中,但并发度低、吞吐受限;适合数据更新频率低、临界区小、或为简单性优先的场景。细粒度锁:用多个锁/分段锁保护不同数据,并发度高、吞吐高,但实现复杂、易出错(死锁、锁顺序)、锁开销大;适合高并发、数据更新频繁、热点分散的场景。取舍:在并发度与复杂度间平衡,先粗后细,用数据监测确定热点再细化。

锁粒度是"并发度 vs 复杂度"的权衡。细粒度提吞吐但增复杂度,需结合热点与竞争压测决定。

#
★★

19. LockSupport.park/unpark 与 Object.wait 的差异

LockSupport.park/unpark 与 Object.wait 的差异是什么?

  • 锁的依赖
  • 许可语义
  • 中断与超时

差异:1) 锁依赖:wait 必须持有监视器(synchronized 内)并释放锁;park 无需持锁。2) 语义:wait 用"等待/通知"机制,park 用"许可(permit)机制",unpark 给许可,park 消费许可,unpark 可先于 park 调用(许可缓存),不会丢失唤醒;wait 的 notify 需在 wait 后。3) 中断:wait 抛 InterruptedException,park 不抛(返回后检查中断标志)。4) 超时:wait(timeout)、parkNanos。park/unpark 更灵活,是 AQS 的底层阻塞原语。

park/unpark 用许可机制、无锁要求、可先 unpark,避免了 wait/notify 的"丢失唤醒"与锁绑定问题,是 AQS 底层。

#

20. JDK 24+(JEP 491)消除 synchronized 钉住后,虚拟线程下 synchronized 与 ReentrantLock 的取舍

JDK 24+(JEP 491)消除 synchronized 钉住后,虚拟线程下 synchronized 与 ReentrantLock 如何取舍?

  • 钉住消除
  • 默认选择
  • 功能需求

JEP 491 后,synchronized 在虚拟线程下不再钉住,作为内置锁简洁、无泄漏风险、可被 JIT 优化,成为虚拟线程下的默认选择。只在需要高级能力时才用 ReentrantLock:可中断获取(lockInterruptibly)、超时(tryLock(timeout))、公平锁、多个 Condition、非阻塞 tryLock。取舍基于"功能需求"而非性能(两者性能已接近)。绝大多数场景用 synchronized 即可。

JEP 491 消除了性能差异,选型回归"是否需要高级锁能力"。默认 synchronized,需要特定能力时 ReentrantLock。

#

21. synchronized 在 JDK 24+ 的 pin 修复(JEP 491)

synchronized 在 JDK 24+ 的 pin 修复(JEP 491)是什么?

  • JEP 491 内容
  • pin 修复
  • 机制

JEP 491(JDK 24)修复了 synchronized 在虚拟线程下的 pin(钉住)问题:虚拟线程在 synchronized 块内阻塞时,JVM 会释放监视器并卸载载体线程,让载体线程执行其他虚拟线程;虚拟线程被唤醒后重新获取监视器继续。这使 synchronized 在虚拟线程下不再占用载体线程,与 ReentrantLock 的卸载行为一致。实现上通过 JVM 的监视器释放/重获机制,在虚拟线程阻塞点改写监视器状态。

pin 修复 = synchronized 阻塞时"释放监视器 + 卸载 + 恢复重获",把 synchronized 变为"可卸载"的锁。

#

22. JEP 491 如何解决虚拟线程在 synchronized 块上的钉住(pinning)问题

JEP 491 如何解决虚拟线程在 synchronized 块上的钉住(pinning)问题?

  • 钉住机制
  • 释放/重获
  • 卸载

JEP 491 的解决思路:当虚拟线程在 synchronized 块内需要阻塞(如 park、IO)时,JVM 检测到该虚拟线程持有监视器,先释放监视器(解除与载体线程的绑定),再卸载载体线程执行其他虚拟线程;当虚拟线程被唤醒时,重新竞争并获取监视器,然后继续执行剩余的同步块。通过"阻塞即释放、恢复即重获"的两阶段,虚拟线程在 synchronized 内阻塞不再钉住载体线程。JVM 需保证重获语义正确(竞争、重入)。

钉住源于"监视器与载体线程硬绑定"。JEP 491 用"阻塞时释放、恢复时重获"打破绑定,实现虚拟线程卸载。

#

23. wait()/notify()/notifyAll() 的合法使用模式

wait()/notify()/notifyAll() 的合法使用模式是什么?

  • 持锁调用
  • 循环条件
  • notify 与 notifyAll

合法模式:1) wait/notify 必须在 synchronized 块内调用(否则 IllegalMonitorStateException);2) wait 必须用 while 循环包裹条件检查(防虚假唤醒与条件被抢先改变);3) notify 只唤醒一个线程,notifyAll 唤醒所有(无法确定哪个条件被满足时用 notifyAll);4) 先改条件再 notify,避免丢失唤醒;5) 用同一把锁保护条件与调用。正确模式确保"唤醒后重新检查条件"。

wait/notify 的正确性依赖"持锁 + 循环条件 + 正确唤醒"。丢唤醒与虚假唤醒都靠 while 循环与先改条件防住。

synchronized (lock) {
    while (!ready) { lock.wait(); } // 必须 while
    doWork();
}