承接的问题
2.1 讲了锁怎么排队,2.2 讲了条件队列怎么转移。还剩三种“等待的边界”没有落到源码:等待能不能被中断、能不能只等一段时间、同一线程能不能重复拿同一把锁。卷一第 6 章讲过中断语义,卷一 9.2 给过 lockInterruptibly() 和 tryLock(timeout) 的用法;本节只回答实现层的问题:
- 中断一个在 AQS 队列里
park的线程,源码怎么把它摘出来? - 超时和中断走的是同一条路径吗?返回值怎么区分?
- 重入计数存在哪里,溢出时为什么抛
Error而不是异常?
版本差异仍然先交代。JDK 8 的超时路径叫 doAcquireNanos,中断路径叫 doAcquireInterruptibly;JDK 25 把两者合并进一个 acquire(Node, int, boolean shared, boolean interruptible, boolean timed, long time) 大循环,上面两个方法名在源码里已经不存在。对外 API acquireInterruptibly 和 tryAcquireNanos 还在,只是内部实现换了。
中断:先检查标志,再进队列
ReentrantLock.lockInterruptibly() 先走 Sync.lockInterruptibly(),再走 AQS 的 acquireInterruptibly():
// JDK 25 源码摘录,已删减
final void lockInterruptibly() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!initialTryLock())
acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted() ||
(!tryAcquire(arg) && acquire(null, arg, false, true, false, 0L) < 0))
throw new InterruptedException();
}
两处 Thread.interrupted() 的作用不同:
Sync.lockInterruptibly()里的那次是入口检查:线程进来时如果已经带着中断标志,直接抛InterruptedException,连锁都不试。这符合 Javadoc 的“中断优先于重入获取”。acquireInterruptibly()里的那次是入队前检查:如果tryAcquire失败,就带着interruptible = true进入acquire主循环;主循环返回负数表示被中断,转成InterruptedException。
Thread.interrupted() 会清除标志,isInterrupted() 不会。JUC 到处用前者,是因为它要把“已经看到中断”这件事收进一个局部布尔变量,避免重复处理。主循环的尾部是这句话:
// JDK 25 源码摘录,已删减
node.clearStatus();
if ((interrupted |= Thread.interrupted()) && interruptible)
break;
|= 是关键:Thread.interrupted() 每次调用都会清标志,如果第一次读到 true,后面再读到 false 也不能把 interrupted 改回去。这个局部变量一直活到循环结束,最后交给取消逻辑:可中断的获取返回负值,让上层抛 InterruptedException;不可中断的 lock() 则把标志重新补回线程,让调用方在拿到锁之后还能看到它。
这解释了一个常见疑问:lock() 不响应中断,但中断并没有丢。线程在 lock() 里被中断时不会抛异常,它会继续等到锁,拿到锁后 Thread.currentThread().isInterrupted() 为 true。这是“延迟响应”,不是“忽略”。
超时:统一 acquire 的三种返回值
tryLock(timeout, unit) 先调 Sync.tryLockNanos(),再调 AQS 的 tryAcquireNanos():
// JDK 25 源码摘录,已删减
final boolean tryLockNanos(long nanos) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return initialTryLock() || tryAcquireNanos(1, nanos);
}
// JDK 25 源码摘录,已删减
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (!Thread.interrupted()) {
if (tryAcquire(arg))
return true;
if (nanosTimeout <= 0L)
return false;
int stat = acquire(null, arg, false, true, true,
System.nanoTime() + nanosTimeout);
if (stat > 0)
return true;
if (stat == 0)
return false;
}
throw new InterruptedException();
}
acquire 的返回值只有三种:正数表示拿到锁,0 表示超时,负数表示被中断。tryAcquireNanos 把正数映射成 true、0 映射成 false、负数落空后抛 InterruptedException。所以超时和中断不是两个分支,而是同一个循环的两种退出原因。
时间参数是绝对截止时间 System.nanoTime() + nanosTimeout,不是每次 park 的相对时间。主循环里用 time - System.nanoTime() 算剩余量,剩余小于等于 0 就 break,再由取消逻辑返回 0。这样即使线程被虚假唤醒多次,总等待时间也不会超过预算。
公平性在这里有一个容易忽略的差异:Sync.tryLockNanos() 先调 initialTryLock()。非公平锁的 initialTryLock() 不查队列,所以带超时的 tryLock 在非公平锁上仍然可以插队;公平锁的 initialTryLock() 会查 hasQueuedThreads(),队列非空时直接失败,进入 tryAcquireNanos 后 tryAcquire 还会再查 hasQueuedPredecessors()。也就是说,公平锁的 tryLock(timeout) 遵守公平,非公平锁的不遵守,而两者共用同一段超时代码。
doAcquireNanos 在 JDK 25 已经不存在;读旧版源码时,把它理解为 acquire(null, arg, false, true, true, deadline) 即可。旧版里“自旋一次、判断截止时间、parkNanos”的三段式,在新版里被压进主循环的 else 分支,逻辑等价。
重入:state 就是计数,溢出是 Error
ReentrantLock 的重入计数直接存在 AQS 的 state 里。NonfairSync.initialTryLock() 的重入分支:
// JDK 25 源码摘录,已删减
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
释放时做减法,只有减到 0 才真正释放:
// JDK 25 源码摘录,已删减
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException();
boolean free = (c == 0);
if (free)
setExclusiveOwnerThread(null);
setState(c);
return free;
}
三个边界:
- 未持有锁就
unlock():getExclusiveOwnerThread() != current,抛IllegalMonitorStateException。这是运行时异常,不是Error,说明是调用方用错了 API。 - 重入次数达到
Integer.MAX_VALUE再加一:state + 1溢出成负数,c < 0命中,抛Error("Maximum lock count exceeded")。用Error而不是异常,是因为此时锁的计数已经不可信,继续运行可能把state解释成“未持有”或“负重入”,属于对象不变量被破坏。 unlock()次数多于lock():第一次多出来的unlock会把state从 0 减到 -1 吗?不会。此时 owner 已经是null,getExclusiveOwnerThread() != current先命中,抛IllegalMonitorStateException,走不到setState(-1)。
Sync.tryLock()(无参 tryLock)的重入分支也有同样的 if (++c < 0) 检查,所以溢出边界对 lock()、tryLock()、lockInterruptibly() 一致。2.2 的 await 用 savedState 保存“当前重入次数”,醒来后通过 reacquire 一次性恢复,正是建立在这个计数语义上。
三项实测
把中断、超时、重入和溢出放进一个程序。溢出部分用反射把 state 直接推到 Integer.MAX_VALUE - 1,避免真的调用二十多亿次 lock():
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
public final class LockProbe {
public static void main(String[] args) throws Exception {
interruptCase();
timeoutCase();
reentrantCase();
overflowCase();
}
static void interruptCase() throws Exception {
ReentrantLock lock = new ReentrantLock();
lock.lock();
CountDownLatch started = new CountDownLatch(1);
AtomicLong caughtAt = new AtomicLong();
Thread waiter = Thread.ofPlatform().name("waiter").start(() -> {
started.countDown();
try {
lock.lockInterruptibly();
lock.unlock();
} catch (InterruptedException e) {
caughtAt.set(System.nanoTime());
System.out.println("interrupt: isInterrupted="
+ Thread.currentThread().isInterrupted());
}
});
started.await();
while (!lock.hasQueuedThread(waiter)) {
Thread.onSpinWait();
}
long t0 = System.nanoTime();
waiter.interrupt();
waiter.join();
System.out.printf("interrupt: elapsedMs=%.1f, isLocked=%s%n",
(caughtAt.get() - t0) / 1_000_000.0, lock.isLocked());
lock.unlock();
}
static void timeoutCase() throws Exception {
ReentrantLock lock = new ReentrantLock();
lock.lock();
AtomicLong elapsed = new AtomicLong();
boolean[] result = new boolean[1];
Thread t = Thread.ofPlatform().start(() -> {
long start = System.nanoTime();
try {
result[0] = lock.tryLock(250, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
elapsed.set(System.nanoTime() - start);
});
t.join();
System.out.printf("tryLock(250ms): result=%s, elapsedMs=%.1f%n",
result[0], elapsed.get() / 1_000_000.0);
lock.unlock();
}
static void reentrantCase() {
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock();
lock.lock();
System.out.println("reentrant: holdCount=" + lock.getHoldCount());
lock.unlock();
lock.unlock();
lock.unlock();
System.out.println("reentrant: isLocked=" + lock.isLocked());
}
static void overflowCase() throws Exception {
ReentrantLock lock = new ReentrantLock();
Field syncField = ReentrantLock.class.getDeclaredField("sync");
syncField.setAccessible(true);
Object sync = syncField.get(lock);
Method setState = Class.forName("java.util.concurrent.locks.AbstractQueuedSynchronizer")
.getDeclaredMethod("setState", int.class);
setState.setAccessible(true);
lock.lock();
setState.invoke(sync, Integer.MAX_VALUE - 1);
lock.lock();
System.out.println("overflow: holdCount=" + lock.getHoldCount());
try {
lock.lock();
} catch (Error e) {
System.out.println("overflow: " + e.getMessage());
}
setState.invoke(sync, 1);
lock.unlock();
System.out.println("overflow: isLocked=" + lock.isLocked());
}
}
编译与运行(反射写 java.util.concurrent.locks 需要 --add-opens,只用于实验,生产代码不要这样做):
javac --release 25 LockProbe.java
java --add-opens java.base/java.util.concurrent=ALL-UNNAMED \
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED \
-cp . LockProbe
本机输出(实验时 load average 约 86):
interrupt: isInterrupted=false
interrupt: elapsedMs=0.1, isLocked=true
tryLock(250ms): result=false, elapsedMs=260.1
reentrant: holdCount=3
reentrant: isLocked=false
overflow: holdCount=2147483647
overflow: Maximum lock count exceeded
overflow: isLocked=false
逐行解释:
- 等待线程被
interrupt()后,caughtAt立刻记录,耗时 0.1 ms;isInterrupted=false说明InterruptedException抛出时清掉了标志。主线程的isLocked=true说明等待者没有拿到锁,catch里不能unlock()。 tryLock(250ms)返回false,实测 260.1 ms,多出的约 10 ms 来自定时器精度和线程被唤醒后重新上 CPU 的调度延迟。高负载下超时只会“晚到”,不会“早退”;如果它远早于 250 ms 返回false,只可能是传入的超时已经小于等于 0,中断会抛InterruptedException,不会返回false。holdCount=3验证重入计数;三次unlock后isLocked=false。- 溢出实验里
holdCount显示2147483647,再lock()抛Error,消息是Maximum lock count exceeded。实验结束前用反射把state重置为 1 再unlock(),否则这把锁会一直处于“持有Integer.MAX_VALUE次”的状态。
在 libraryd-conc 里落地
LockedLoanService 的 reserve() 已经在 2.1 用了 tryLock(200ms):等不到锁就返回 CONFLICT,由上层决定重试或降级。returnBook() 更适合可中断等待,因为归还通常发生在请求线程或关闭流程里:
// LockedLoanService 中替换后的 returnBook 方法
@Override
public boolean returnBook(String loanId) {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LoanException(ErrorCode.INTERRUPTED, "归还时等待锁被中断");
}
try {
return loans.findById(loanId)
.map(loan -> {
loans.save(loan);
return true;
})
.orElse(false);
} finally {
lock.unlock();
}
}
这段代码的边界很清楚:
- 中断只可能发生在
lockInterruptibly()里,也就是还没拿到锁、还没改任何状态的时候;此时抛INTERRUPTED不会留下半完成的归还记录。 - 一旦进入
try块,临界区里的代码不响应中断;finally保证释放锁。如果临界区里有 IO 或远程调用,中断要等它返回后才生效,这也是“锁内不做慢操作”的另一个理由。 reserve()的tryLock(200ms)与这里形成互补:读路径怕排队,写路径怕不能取消。两者都失败时,LoanException的ErrorCode分别是CONFLICT和INTERRUPTED,上层可以据此决定重试还是终止。
如果服务要优雅停机,线程池 shutdownNow() 会给工作线程发中断;正在 lockInterruptibly() 里排队的归还请求会立刻退出,而不是等到停机超时。这个行为依赖 returnBook() 没有吞掉 InterruptedException,也没有在 catch 里继续重试。
这个设计的代价
- 可中断不等于可取消。 中断只保证“等待阶段”能退出;如果线程已经进入临界区,中断要等临界区结束。把
lockInterruptibly()当成“随时能停”的保证,会在长临界区上失算。 - 超时不是重试策略。
tryLock(200ms)返回false只说明这段时间没抢到锁,不说明服务过载;直接循环重试会把锁竞争放大。正确做法是上层给总预算,超时后降级或快速失败。 - 吞掉
InterruptedException会破坏停机。catch里既不恢复标志也不抛出,线程池会以为任务正常结束,shutdownNow()的传播链在这里断掉。至少要Thread.currentThread().interrupt()。 - 重入是便利,不是设计目标。 深层重入会让锁的持有时间难以估算,也容易掩盖锁顺序问题;能拆成“外层持锁、内层无锁方法”就不要依赖重入计数。
- 溢出
Error不可恢复。 不要catch (Error)后继续用这把锁;对象不变量已经坏了,正确做法是让线程退出、重建服务实例。这个边界现实中几乎碰不到,但源码明确把它定义成Error,读源码时要理解这个选择。
小结
lockInterruptibly()在入口用Thread.interrupted()清标志并优先抛异常;acquire主循环用interrupted |= Thread.interrupted()累积中断,可中断时返回负值,不可中断时把标志补回。tryLock(timeout)走tryAcquireNanos,内部是带绝对截止时间的统一acquire;返回值正数成功、0 超时、负数中断。JDK 25 已没有doAcquireNanos。- 重入计数就是 AQS 的
state;溢出抛Error("Maximum lock count exceeded"),未持锁unlock()抛IllegalMonitorStateException。 - 本机实测:中断等待线程 0.1 ms 退出且锁仍由主线程持有;
tryLock(250ms)在 260.1 ms 返回false;重入 3 层计数正确;state推到Integer.MAX_VALUE后下一次lock()抛Error。
阅读导航:上一节 2.2 Condition 的条件队列 | 下一节 3.1 ReentrantReadWriteLock 的 state 拆分
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。