锁语义与公平性:synchronized、ReentrantLock 与 ReadWriteLock
版本基线:语言语义以 JLS SE 17、JVMS SE 17 为准,API 契约以 Java SE 17 为准,实现分析固定到 OpenJDK 17u
jdk-17.0.12+7。本机实际编译运行版本为 Oracle JDK 17.0.12+8(x86_64,/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home)。happens-before/volatile/final 的推导权威在JMM 正文;AQS 排队骨架与 Condition 的源码链在AQS 正文;CountDownLatch/CyclicBarrier/Semaphore/Phaser/Exchanger 在同步器正文。本页讲锁这一层:锁对象语义、重入与公平、可中断与超时获取、读写锁与乐观读、死锁诊断与恢复。虚拟线程时代 synchronized 的另一条版本边界(JDK 21 阻塞时 pin 住 carrier、JEP 491 于 24 消除)由虚拟线程正文讲清,本页 17 基线的锁语义不受影响。
一把锁要同时解释互斥、发布和进展
假设两个请求更新同一账户的内存余额和流水计数。仅把余额声明为 volatile,只能改善单字段可见性,不能把"检查余额、扣减、增加计数"变成一个不可分割动作。应先定义不变量:余额非负、扣减与流水计数共同更新、读者不能看到半次更新,再选择保护完整操作的锁。锁名里出现 concurrent 或底层使用 CAS 都不构成这个业务证明。
同一把锁的释放与之后成功获取建立可见性关系;互斥让临界区不会被另一个持有者同时执行。二者缺一不可。写者锁住 A 而读者锁住 B,或把写入放在锁外,都不能借"代码用了锁"建立同步关系(监视器 happens-before 边的规范推导见JMM 正文,本页不重复)。JLS 17.1、17.4.5规定监视器;显式锁的内存语义见 Lock 接口。
第三个问题是进展:获取能否超时、能否响应中断、持有者退出是否释放、等待者是否可能饥饿。公平锁减少插队,不等于操作系统给每个线程相同 CPU,也不保证业务时延上界。锁内做远程 I/O 会让外部延迟变成所有竞争者的等待;先问临界区是否必须覆盖 I/O,比先问换哪种锁更有价值。这三问(互斥、发布、进展)是本页三个族群共用的评审框架:synchronized、ReentrantLock 与 ReentrantReadWriteLock 在每一问上的答案不同,但问题相同。
synchronized:锁对象、字节码入口与监视器慢路径
同步块锁住表达式求得的对象身份;实例同步方法锁住接收者,静态同步方法锁住声明类的 Class 对象。两个不同实例的方法锁通常彼此独立;一个实例上的静态方法与实例方法也不是同一把锁。不要锁装箱整数、字符串常量或每次重新创建的对象:共享身份和生命周期不清楚,会造成意外竞争或根本没有共同保护。
同步块通常编译为 monitorenter/monitorexit,编译器还安排异常出口释放;同步方法通过方法标志表达同步,不能要求每个同步方法的字节码里一定出现这两个指令。JVMS monitorenter 与 JLS 同步语句 分别说明指令与语言语义。正常返回、抛异常都会退出已进入的同步区,但阻塞调用长时间不返回时,锁当然仍被持有。
HotSpot 入口可沿 ObjectSynchronizer::enter 追到膨胀后的 ObjectMonitor::enter → EnterI → TryLock / park;wait/notify 则走 ObjectSynchronizer::wait/notify → ObjectMonitor::wait/notify → INotify。源码分别为 synchronizer.cpp 与 objectMonitor.cpp。这是运行时慢路径的阅读入口;解释器和编译器还有快速路径,不能说每次同步都立即进入重量级监视器。
按该标签实现精简的教学伪代码如下,省略栈锁所有者转换、监视器回收与平台细节:
进入已膨胀监视器:
owner为空 -> CAS尝试取得所有权
owner是当前线程 -> 增加递归计数
其他线程持有 -> 尝试自旋,失败后进入竞争集合并park
被唤醒 -> 重试取得owner,不是直接拥有锁
退出:
仍有递归层数 -> 仅减一层
最后一层 -> 释放owner,按竞争状态安排后继这里最重要的分支是重入只减少层数,和唤醒后还要重新竞争。监视器内部的 _cxq、_EntryList、_WaitSet 承担不同用途,不应画成一个严格 FIFO 业务队列。自旋只在有希望很快取得锁时节约阻塞成本,持有者被抢占、CPU 紧张或临界区很长时,继续自旋可能浪费 CPU。
版本边界与"锁升级"误区:对象头位布局、偏向/轻量级/重量级状态转换属于该实现,不是语言契约。偏向锁(biased locking)在 JDK 15 起默认禁用(JEP 374:竞争下的撤销成本高于收益),JDK 17 不会再"从偏向锁开始升级"。把"无锁 → 偏向 → 轻量级 → 重量级"背成固定流水线是跨版本错误:不同版本默认起点不同,转换条件也不构成规范承诺。跨大堆、竞争剧烈时对象可能直接以膨胀监视器创建。评审时应问"这个结论依赖哪一版 HotSpot",而不是复述升级链。
wait、notify 与丢通知的条件协议
调用某对象的 wait/notify 必须拥有该对象的监视器,否则会抛 IllegalMonitorStateException。wait 把线程放入等待集合,释放该对象上所有重入层数,返回前重新取得同一监视器并恢复层数;它不会释放线程同时持有的其他锁。sleep 不具备这种释放监视器的语义。Object.wait/notify 契约 是判断先后顺序的依据。
notify 只是让等待者有资格重新竞争,不释放通知者的锁,也不保存一份供未来等待者消费的"通知余额"。正确写法是在同一锁内修改条件、在 while 循环中检查条件;条件不满足才 wait。若先在锁外检查队列空,再获取锁等待,生产者可能在间隙中生产并通知,消费者随后才等待,形成丢失唤醒。本票实验用两个闩锁把"生产者信号已发、消费者才开始等"钉成确定性交错,实测 await 一直等到超时——信号没有余额可补(实验第 2 节,见AQS 正文的 signal 机制与恢复写法;该实验对 wait/notify 与 Condition 同构)。
ObjectMonitor 的 INotify 从 WaitSet 取出等待者并转向进入竞争路径;notify 本身先校验所有者,WaitSet 为空就返回。因此"先 notify 再 wait 也能收到之前通知"不是这套协议。虚假唤醒、通知竞争与其他消费者先取走资源都会要求再次检查业务谓词。只有一种条件且确定任意等待者都可推进时单个 notify 才容易证明;混合生产者/消费者共用等待集合时,notifyAll 往往更容易避免唤醒错误角色,但会增加竞争。显式 Condition 的两类队列转移、signal 与中断竞争的源码链在AQS 正文,本页只保留监视器协议的对照。
ReentrantLock:重入计数、公平门与插队契约
固定标签的 ReentrantLock.java 中,外层锁委托给 Sync,Sync 继承 AQS;state 表示持有次数,exclusiveOwnerThread 表示持有者。主链是 lock → Sync.lock → initialTryLock → AQS.acquire → tryAcquire,释放为 unlock → release → tryRelease → signalNext(排队与释放骨架见AQS 正文)。
公平与非公平的差异全部写在 initialTryLock/tryAcquire 的判定顺序里,不是"CAS 指令不同"。非公平版第一枪直接 CAS:
原码连续摘录(仅统一公共缩进):ReentrantLock.java,jdk-17.0.12+7,L223–L236。
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) { // first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}注释原话 first attempt is unguarded:只要 state==0 就 CAS,不检查队列里有没有等待者——这正是"插队"(barging)的实现位置。竞争刚释放、等待者尚未被唤醒的窗口里,新到线程能直接拿走锁。公平版把同一个入口装上队列门:
原码连续摘录(仅统一公共缩进):ReentrantLock.java,jdk-17.0.12+7,L259–L274。
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}两个判定:快速路径查 !hasQueuedThreads()(队列非空就不试),排队后 tryAcquire 查 !hasQueuedPredecessors()(自己不是队首就不试;该方法源码见AQS 正文)。公平锁用吞吐换等待顺序:每次获取都要过队列检查,且释放-唤醒窗口内不允许新来者插队,上下文切换更频繁。实测口径(实验第 3 节):公平锁下按序入队的 4 个线程严格按入队顺序获得锁;非公平锁下,持有者释放后立刻重新 lock() 会在已排队线程醒来之前把锁拿回去——这是设计行为,不是缺陷。
该标签把重入判断放在 initialTryLock 等路径,后续非公平 tryAcquire 主要处理非重入竞争;很多 JDK 8 讲解把所有逻辑写在 nonfairTryAcquire 里,与这里的符号和调用顺序并不相同。阅读源码应固定 tag 后逐函数追踪,而不是把熟悉的方法名强行套进去。
三条使用契约(ReentrantLock API):
- 无时限
tryLock()在公平锁上也允许插队。它"恰好可用就获取"的语义优先于公平策略;需要遵守公平策略的限时形式是tryLock(long, TimeUnit)。实测(实验第 3 节):公平锁上已有排队线程、锁刚释放时,无时限tryLock()立即成功——契约如此,别拿它当公平获取用。 - 非持有者
unlock()抛IllegalMonitorStateException;tryLock返回 false 的分支不得解锁。习惯写法是成功lock后立即进入try/finally。 lock()不因中断抛出,lockInterruptibly()支持取消获取;等待取消与临界区内业务取消是两个层次(下一节)。
可中断与带时限的获取:把进度抓回自己手里
无等待上限的获取把进程的存续押在"对方迟早释放"上。持有者卡在远程调用、死锁或无限循环时,lock() 的等待者只能一起挂。三种把上界拿回来的手段,语义各不相同:
| 方法 | 返回/异常 | 拿到锁了吗 | 适用 |
|---|---|---|---|
tryLock() | boolean | true 才持有 | 非阻塞探测;允许插队(公平锁亦然) |
tryLock(time, unit) | boolean | true 才持有 | 限时获取;超时不修改任何状态 |
lockInterruptibly() | InterruptedException | 异常时绝不持有 | 可取消的无限期获取 |
两条容易被忽视的边界。第一,超时/中断失败后不得有部分修改:tryLock 超时返回 false 与 lockInterruptibly 抛出时,线程不持有锁,也不应已改写受锁保护的任何字段——把"准备计算"放锁外、"提交结果"放锁内,失败路径才是干净的。第二,限时获取的上界只覆盖等锁:tryLock(1, SECONDS) 成功后临界区执行多久、awaitNanos 返回后重获锁要多久,都不在承诺内;需要总时长上界要用外层 deadline 每轮重算剩余时间(awaitDeadline 模式)。
本票实验第 4 节实测:主线程持锁时,工作线程 tryLock(200ms) 返回 false 且锁仍被持有;释放后限时获取成功;对正在限时等待的线程发中断,tryLock 抛 InterruptedException 且不持锁。lockInterruptibly 在持锁者不放手的输入下抛出而不进入临界区(SeniorProbe locks 模式历史输出同结论)。线程级 interrupt 机制与线程池取消归线程池与异步正文,本页只讲获取路径上的取消。
读多写少:一个 int 装下两把锁
ReentrantReadWriteLock 把"读-读可并行、读-写/写-写互斥"编码进同一个 AQS state:
原码连续摘录(仅统一公共缩进,截取字段声明):ReentrantReadWriteLock.java,jdk-17.0.12+7,L263–L271。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count. */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count. */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }高 16 位是共享(读)持有总数,低 16 位是独占(写)重入层数。写获取要求 c != 0 时必须"写计数非零且所有者是自己"(重入),否则失败——持读锁的线程去拿写锁会永久等待(自己挡自己),文档称之为会死锁,不能当成"升级"。每个线程自己的读重入层数由 ThreadLocal 的 HoldCounter 记录(getReadHoldCount),因为 state 只存总数。
降级(downgrade)是唯一被支持的顺序:持写锁 → 获取读锁 → 释放写锁 → 最后释放读锁。官方示例的注释就是教材:
原码连续摘录(仅统一公共缩进):ReentrantReadWriteLock.java,jdk-17.0.12+7,L143–L159。
* rwl.readLock().lock();
* if (!cacheValid) {
* // Must release read lock before acquiring write lock
* rwl.readLock().unlock();
* rwl.writeLock().lock();
* try {
* // Recheck state because another thread might have
* // acquired write lock and changed state before we did.
* if (!cacheValid) {
* data = ...;
* cacheValid = true;
* }
* // Downgrade by acquiring read lock before releasing write lock
* rwl.readLock().lock();
* } finally {
* rwl.writeLock().unlock(); // Unlock write, still hold read
* }三步都不能省:先放读锁再拿写锁(否则自锁);拿到写锁后重查条件(等待期间别人可能已写入);降级期间保持读锁,让"写入 → 自己继续读"之间不被插队。实测(实验第 5 节):双读者并发计数峰值 2;读者持锁时写者限时获取 false;降级序列成功且数据一致;持读锁时 tryWriteLock(150ms) 返回 false(若用无时限 writeLock() 即自死锁,实验用限时形式兜底)。
非公平版的写者优先倾向写在两个 shouldBlock 钩子里:
原码连续摘录(仅统一公共缩进,截取嵌套类局部):ReentrantReadWriteLock.java,jdk-17.0.12+7,L680–L707。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}非公平写者永远可以插队(writers can always barge),读侧只做启发式让步(队首若是写者,新读者排队)——注释自己承认"仅是概率性效果"。所以非公平 RWL 减少写者饥饿但不消除;公平版两个方向都查 hasQueuedPredecessors(),吞吐代价与公平 ReentrantLock 同源。读多写少也非读写锁的充分条件:读者持锁过久仍阻塞写者,写者阻塞又会排队读请求;临界区极小时一把 ReentrantLock 或不可变快照(见JMM 正文的发布链方案)常常更快。
StampedLock 是读写之外的第三档:写锁(非重入、返回 stamp)、读锁,以及乐观读。乐观读不获取锁,只取一个版本票据,用完验证:
原码连续摘录(仅统一公共缩进):StampedLock.java,jdk-17.0.12+7,L615–L641。
/**
* Returns a stamp that can later be validated, or zero
* if exclusively locked.
*
* @return a valid optimistic read stamp, or zero if exclusively locked
*/
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
/**
* Returns true if the lock has not been exclusively acquired
* since issuance of the given stamp. Always returns false if the
* stamp is zero. Always returns true if the stamp represents a
* currently held lock. Invoking this method with a value not
* obtained from {@link #tryOptimisticRead} or a locking method
* for this lock has no defined effect or result.
*
* @param stamp a stamp
* @return {@code true} if the lock has not been exclusively acquired
* since issuance of the given stamp; else false
*/
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}正确顺序是"取 stamp → 读字段到局部 → validate(stamp) → 通过才使用局部值;失败升级为读锁或重试"。验证通过前不得基于乐观读执行外部副作用:验证失败说明版本已失效,此前读到的可能是半更新状态,副作用无法回滚。validate 先 loadFence 再比较版本位,保证字段读取不被重排到验证之后。三条边界:StampedLock 不可重入(持写锁再 writeLock() 自死锁,实测限时形式返回 false);不支持 Condition;乐观读不是锁,不提供互斥,只提供"读期间没有写"的版本验证。实测(实验第 6 节):乐观读期间发生写,validate 返回 false;升级悲观读后值一致。
死锁:实例、诊断与恢复
死锁的规范定义就是ThreadMXBean描述的"持有-等待环":
原码连续摘录(仅统一公共缩进,截取接口文档与方法声明):ThreadMXBean.java,jdk-17.0.12+7,L628–L655。
/**
* Finds cycles of threads that are in deadlock waiting to acquire
* object monitors or
* <a href="LockInfo.html#OwnableSynchronizer">ownable synchronizers</a>.
*
* Threads are <em>deadlocked</em> in a cycle waiting for a lock of
* these two types if each thread owns one lock while
* trying to acquire another lock already held
* by another thread in the cycle.
* <p>
* This method is designed for troubleshooting use, but not for
* synchronization control. It might be an expensive operation.
*
* @return an array of IDs of the threads that are
* deadlocked waiting for object monitors or ownable synchronizers, if any;
* {@code null} otherwise.
*
* @throws SecurityException if a security manager
* exists and the caller does not have
* ManagementPermission("monitor").
* @throws UnsupportedOperationException if the Java virtual
* machine does not support monitoring of ownable synchronizer usage.
*
* @see #isSynchronizerUsageSupported
* @see #findMonitorDeadlockedThreads
* @since 1.6
*/
public long[] findDeadlockedThreads();两个关键词:循环(cycle)与两类锁(object monitors = synchronized,ownable synchronizers = j.u.c 锁)。findMonitorDeadlockedThreads 是 JDK 1.4 的旧接口,只查监视器,查不到 ReentrantLock 死锁;诊断 j.u.c 锁必须用 findDeadlockedThreads。且文档明说这是排障工具,不是同步控制手段。
实验第 7 节的完整闭环(本机 Oracle JDK 17.0.12+8):T1 持锁 A 限时争锁 B,T2 持锁 B 限时争锁 A;轮询到双线程都阻塞后:
ThreadMXBean.findDeadlockedThreads()返回 2 个线程 id(断言);- 同 JDK 的
jstack <pid>子进程输出包含Found one Java-level deadlock,并逐线程打印waiting for ownable synchronizer 0x...与held by(真实工具输出); - 等待超时后双方退避(释放自己持有的锁、按全局顺序 A→B 重试),第二轮成功——恢复不是靠打破对方,而是靠超时感知+改顺序重试。
线程 dump 的读法:从 Found one Java-level deadlock 往下,每个线程一段"等什么(锁地址/类型)→ 谁持有(线程名)",沿 held by 链走一圈即环;Java stack information 给出持锁与等锁的源码行号,定位到业务代码的获取顺序。synchronized 死锁显示为 waiting to lock <0x...>(monitor),j.u.c 锁显示为 waiting for ownable synchronizer。
预防与恢复的四个工程手段,各有代价:
| 手段 | 做法 | 代价/边界 |
|---|---|---|
| 锁排序 | 全局固定获取顺序(如按资源 id 升序) | 跨键操作要重排逻辑;顺序约定要覆盖所有路径 |
| tryLock 超时 + 退避 | 限时获取失败就释放已持有的锁,退避后重试 | 活锁风险:双方同节奏退避会互相错过;退避要加随机抖动 |
| 开放调用(open calling) | 锁内不调用会获取其他锁/未知代码的方法 | 需要把"锁内校验+锁外执行"拆开,可能引入重查与版本校验 |
| 单锁/合并 | 用一把粗锁消除环 | 并行度下降;回到容量问题 |
synchronized 死锁不可恢复:monitor 获取不能超时、不能中断,只能重启进程。这也是"诊断能力影响锁选型"的实例:需要在线解死锁的路径(如多资源编排)用可超时的显式锁;纯内部短临界区仍可用 synchronized。活锁与死锁的区别:线程没阻塞但互相谦让/重试永不收敛,dump 里没有 deadlock 环,只能靠进度指标发现。
选型算例:2ms 临界区、600 次/秒与公平锁
假设单锁保护一次平均 2ms 的更新,锁外逻辑可忽略,理论服务能力约 500 次/s;如果到达率升到 600 次/s,排队会持续增长。改成公平锁不能突破串行工作量。若业务能按账户分片为 16 把锁,且键分布均匀,可增加并行;但热点账户仍受单键串行限制,跨账户转账还需要固定锁顺序或数据库事务(锁排序即上节死锁预防)。
条件改为临界区包含 100ms 远端调用,首先判断能否在锁内只取版本快照、锁外调用、回锁后校验版本再提交。这样缩短持锁,但会引入版本冲突、重试和外部副作用重复问题;不可撤销支付不能仅靠"版本不一致就重试"。另一方案是按键串行事件队列,代价是队列容量、任务持久化与故障重放责任。
读多写少可评估读写锁,但读者持锁过久仍阻塞写者;不能在持读锁时无协议地升级写锁(上一节的自锁与降级顺序)。StampedLock 乐观读需要先取局部快照再 validate,失败后重读,验证前不得执行外部副作用;它不提供 ReentrantLock 式重入语义。此处是选型边界,验证仍应以目标实现和实际读写比例为准,见 StampedLock API。
条件改为要求请求取消,应优先选择能将剩余预算传给 tryLock/await 的协议,并保证获取失败不会部分修改状态。指标要覆盖持锁时长分布、等待时间、超时、中断、队列长度和热点键,不用一个平均等待数掩盖尾部。调度、容器 CPU 节流和远端阻塞都可能拉长持锁时间,换锁前先取得线程与时间线证据(jstack 采样见上节)。
实验:锁语义、公平性与死锁诊断
公开入口(仓库根):
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home \
bash labs/java-senior/run.sh sync探针为 labs/java-senior/LocksProbe.java。本节相关输出(2026-10-25,Oracle JDK 17.0.12+8,macOS 26.7.1 x86_64):
== 3. fair vs nonfair: queue order and barging ==
fair: 4 queued workers acquired in enqueue order [W1, W2, W3, W4](断言:严格 FIFO)
fair: main relock() after unlock queued behind workers(公平门 hasQueuedThreads/hasQueuedPredecessors)
nonfair: main relock() barged ahead of queued waiter in 3/3 rounds(无守卫 CAS 允许插队——设计行为,观察值)
fair untimed tryLock() barged ahead of queued waiter in 3/3 rounds(无时限 tryLock 恒为非公平实现,契约行为)死锁诊断输出(实验第 7 节,jstack 为真实子进程输出节选):
ThreadMXBean.findDeadlockedThreads() -> 2 threads(持有-等待环检出,断言)
jstack: Found one Java-level deadlock: "dl-T1": waiting for ownable synchronizer
... which is held by "dl-T2"(互持互等环 + 持有线程名,真实子进程输出)
deadlock window ended: timeouts=2; both recovered=true(超时退避+固定顺序 A->B 重试恢复,断言)断言口径:公平排序、限时/中断失败不持锁、读写互斥与降级、乐观读失效、死锁检出与恢复全部断言(确定性或大裕度);插队与耗时按 3 轮观察值记录(非公平插队在 3/3 轮出现,属设计行为非概率断言)。死锁线程全部 tryLock(6s) 限时兜底且有闸门固定持有窗口,实验不依赖杀进程。历史证据:SeniorProbe locks 模式(有界缓冲 500500、lockInterruptibly)为 U08 之前的输出,原样保留;run.sh all 同时运行新旧探针。完整 12 节输出、负例与红绿记录见实测证据。
边界:公平/非公平对比是单机单轮时序,非吞吐基准;HotSpot 监视器路径未在实验里反射验证(正文以固定 tag 源码为据);jstack 自附加在本机 Oracle JDK 可用,容器/受限环境可能需要 ThreadMXBean 替代(实验两者都验证)。
追问链
第一轮:为什么公平锁"更慢"却仍存在?
公平性把等待顺序变成可预测的 FIFO,代价是每次获取过队列检查、释放-唤醒窗口不让新来者插队,吞吐下降、上下文切换增多。存在的原因:个别请求不能被无限插队饿死(如配额申领、任务分派),需要的是等待顺序保证而非时延保证。评分点:插队窗口在哪(first attempt is unguarded)、公平检查在哪、代价是什么。
第二轮:持读锁时想写,为什么"升级"会死锁?
写获取要求低 16 位写计数非零且所有者是自己,或 state==0;持读锁时 state 高 16 位非零,自己又是"众多读者之一",写获取等待"所有读者退出",而自己正持读不退——环成立。正确路径只有降级:先拿写锁再拿读锁,或先放读锁再拿写锁后重查条件。评分点:state 位拆分、自锁链、官方降级示例的三步。
第三轮:热点键使服务 P99 超标,换公平锁能否解决?
不能消除串行瓶颈,应衡量持锁工作量与负载,比较缩小临界区、分片和串行队列,解释跨键操作与故障恢复。评分点:定量容量、热点不可均摊、业务副作用。
第四轮:线上疑似锁死锁,取证顺序是什么?
先 jstack(或 ThreadMXBean.findDeadlockedThreads())拿持有-等待环与源码行号,确认是 monitor 还是 ownable synchronizer、环涉及哪些业务路径;再按"锁排序/超时退避/开放调用/合并锁"评估改造;synchronized 环无法在线打破,只能重启+改代码。评分点:两类死锁的检测差异、恢复手段与代价、取证先于修复。
闭卷验收
本页 13 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
| 来源 | 版本 | 用途 |
|---|---|---|
| JLS SE 17 §17.4.5 happens-before | Java SE 17 | 监视器 unlock→lock 可见性边(推导在 JMM 正文) |
| JLS SE 17 §14.19 synchronized 语句 | Java SE 17 | 同步块的锁对象与异常退出 |
| JVMS SE 17 monitorenter | Java SE 17 | 监视器指令语义 |
| Java SE 17 Object API | Java SE 17 | wait/notify 的所有权与等待语义 |
| Java SE 17 Lock API | Java SE 17 | tryLock/lockInterruptibly 的取消与失败语义 |
| Java SE 17 ReentrantLock API | Java SE 17 | 公平性、可中断获取与 tryLock 差异 |
| Java SE 17 ReentrantReadWriteLock API | Java SE 17 | 降级示例、升级自锁、读写语义 |
| Java SE 17 StampedLock API | Java SE 17 | 乐观读、验证与非重入边界 |
| Java SE 17 ThreadMXBean | Java SE 17 | 死锁检测范围(monitor + ownable synchronizer) |
| JEP 374: Disable and Deprecate Biased Locking | JDK 15 | 偏向锁默认禁用的版本事实 |
| OpenJDK 17u ReentrantLock | jdk-17.0.12+7 | 非公平 initialTryLock(L223–L236)、公平 initialTryLock(L259–L274) |
| OpenJDK 17u ReentrantReadWriteLock | jdk-17.0.12+7 | state 位拆分(L263–L271)、降级示例(L143–L159)、公平/非公平 shouldBlock(L680–L707) |
| OpenJDK 17u StampedLock | jdk-17.0.12+7 | tryOptimisticRead/validate(L615–L641) |
| OpenJDK 17u ThreadMXBean | jdk-17.0.12+7 | findDeadlockedThreads 文档(L628–L655) |
| OpenJDK 17u HotSpot synchronizer.cpp | jdk-17.0.12+7 | 监视器进入/等待运行时入口 |
| OpenJDK 17u HotSpot objectMonitor.cpp | jdk-17.0.12+7 | 膨胀监视器的竞争与通知路径 |
本地 LocksProbe 实验 | Oracle JDK 17.0.12+8 | 公平排序/插队/限时中断/读写锁/乐观读/死锁诊断恢复实测输出 |
本地 SeniorProbe locks 历史 | Oracle JDK 17.0.12 | 有界缓冲与可中断获取(U08 前输出,原样保留) |
访问日期:2026-10-25。HotSpot 监视器内部结构属于实现细节,不是语言契约;换 JDK 版本需重新核对。公平/非公平与读写锁实验为单机单轮时序观察,断言压在契约与确定性顺序上,不作吞吐外推。