LockSupport
LockSupport 源码分析
线程阻塞/唤醒的最底层积木:AQS、ReentrantLock、Semaphore 里所有”排队睡觉”的动作最终都落到这里的 park/unpark。核心思路是给每个线程配一张”许可”(permit):unpark 发许可(最多攒一张),park 消费许可,有就立刻返回,没有才真正挂起——顺序无关,谁先到都不丢唤醒。
代码块收起展开
// 基于本地 JDK 源码 (D:/1ForCode/JAVA_Source, 含虚拟线程支持, JDK 21+), java.util.concurrent.locks.LockSupport
public final class LockSupport {
private LockSupport() {} // Cannot be instantiated.
// 注意:纯静态工具类,"许可"状态根本不存在这个类里——它挂在每个平台线程的原生 Parker 结构上
private static void setBlocker(Thread t, Object arg) {
U.putReferenceOpaque(t, PARKBLOCKER, arg); // 按偏移量直写 Thread.parkBlocker 字段;opaque 足够,它不参与同步,只给诊断工具看
}
// ... setCurrentBlocker(Object) 省略:给无参 park() 手动补 blocker 用的
public static void unpark(Thread thread) {
if (thread != null) { // 传 null 静默返回,调用方(如 AQS 找后继节点)不用先判空
if (thread.isVirtual()) {
JLA.unparkVirtualThread(thread); // 虚拟线程走 JVM 内部调度器,不碰内核 Parker
} else {
U.unpark(thread); // 把目标线程的许可置为"有"(已有则不变,不累加);若它正阻塞在 park 上则唤醒
}
}
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker); // 挂起前记下"因谁而堵",jstack 里显示 parking to wait for <blocker>
try {
if (t.isVirtual()) {
JLA.parkVirtualThread();
} else {
U.park(false, 0L); // false=相对时间,0=无限等。有许可则消费掉立刻返回,否则真正挂起
}
} finally {
setBlocker(t, null); // 无论怎么醒的都必须清掉,否则诊断信息"过期",看到的是上一次的 blocker
}
}
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) { // 非正数直接返回,连许可都不碰
Thread t = Thread.currentThread();
setBlocker(t, blocker);
try {
if (t.isVirtual()) {
JLA.parkVirtualThread(nanos);
} else {
U.park(false, nanos); // 相对超时版,AQS 的 tryAcquireNanos / 带超时的 lock 走这里
}
} finally {
setBlocker(t, null);
}
}
}
// ... parkUntil(Object blocker, long deadline) 省略:U.park(true, deadline),true=绝对毫秒时间戳
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return U.getReferenceOpaque(t, PARKBLOCKER); // 瞬时快照:读到值时线程可能已经醒了或换了地方堵
}
public static void park() { // 无 blocker 版本:jstack 里只剩一行 parking,定位困难,锁实现里应优先用带 blocker 的重载
if (Thread.currentThread().isVirtual()) {
JLA.parkVirtualThread();
} else {
U.park(false, 0L);
}
}
// ... parkNanos(long) / parkUntil(long) 省略:同上两个,只是不设 blocker
// Hotspot implementation via intrinsics API
private static final Unsafe U = Unsafe.getUnsafe();
private static final long PARKBLOCKER
= U.objectFieldOffset(Thread.class, "parkBlocker"); // 类加载时算好字段偏移,之后绕过反射直写内存
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
}原理串讲
走一遍最典型的链路:ReentrantLock.lock() 抢锁失败,AQS 把当前线程包成节点入队,在 acquire 循环里调 LockSupport.park(this)。
park 先 setBlocker 把 AQS 同步器对象挂到 Thread.parkBlocker 上,然后 U.park(false, 0L) 进入 HotSpot:每个平台线程的原生结构里带一个 Parker,本质是”一个 0/1 计数 + 一把系统级 mutex/条件变量”。
park 先看计数,是 1 就原子清零直接返回(许可被消费);是 0 才真正陷入内核睡眠。
另一边,持锁线程 unlock() 走到 tryRelease 成功后找到队列后继,调 LockSupport.unpark(s.thread),U.unpark 把那个线程的计数置 1 并 signal——被挂起的线程醒来,回到 AQS 循环开头,重新 tryAcquire。
第一个为什么:为什么 AQS 选 park/unpark 而不用 wait/notify。
wait/notify 有个绕不开的前置——必须先 synchronized 持有监视器锁,而 AQS 本身就是用来实现锁的,实现锁不能依赖另一把锁。
更致命的是顺序问题:notify 若发生在 wait 之前会直接丢失,且 notify 只能唤醒监视器上”某一个”等待者,无法点名。
释放锁的线程和刚要挂起的排队线程之间存在天然竞态(检查条件之后、park 之前,unpark 可能恰好插进来),许可模型把这个窗口消掉了:先到的 unpark 存成一张许可,随后的 park 消费掉直接返回,唤醒不丢;unpark(thread) 还能精确点名到线程。
无锁前置、顺序无关、可点名,三条正好是队列同步器要的。
第二个为什么:为什么许可最多攒一张、不像 Semaphore 那样计数。javadoc 里说得很直白,park 的定位是”busy wait 的优化”——正确性由外层的 volatile 条件加循环保证,许可只负责”别把那一次唤醒弄丢”。
代码块收起展开
一张许可刚好覆盖"unpark 插在检查条件与 park 之间"这个竞态窗口;如果攒 N 张,后续 N 次 park 都会空穿而过,等于制造了 N 次虚假唤醒。
所以标准用法永远是 `while (!canProceed()) LockSupport.park(this);`,AQS 的 acquire 循环就是这个形状。第三个为什么:park 返回后为什么必须重查条件。park 返回有三种原因——被 unpark、被中断、无理由的虚假返回——而它一概不报告是哪种,中断返回时甚至不抛 InterruptedException 也不清中断标志。
再叠加许可可能是上一轮遗留的,返回这个动作本身不携带任何”条件已满足”的信息。把 park 当”睡到条件满足”用是最常见的误用,它的语义只有”睡到某次可能相关的动静为止”。
parkBlocker 则完全是另一条线:它不参与任何同步决策,唯一的消费者是 jstack、JMC 这类监控诊断工具——线程 dump 里那行 parking to wait for <0x...> (a ReentrantLock$NonfairSync) 就是从这个字段读出来的。
所以写它用 putReferenceOpaque 就够,不需要 volatile 的内存屏障开销;放在 finally 里清空,是保证工具读到非 null 时线程确实(大概率)还堵在这个对象上。
设计取舍
- 许可上限 1 张,只为覆盖 park 前的竞态窗口;需要计数语义直接用 Semaphore,别拿 unpark 攒次数。
- unpark 先于 park 有明确保证(下一次 park 不阻塞);但 unpark 一个还没 start 的线程,效果不保证。
- park 被中断会返回,但不抛异常、不清标志,要自己
Thread.interrupted()判断——AQS 可中断与不可中断两套 acquire 的分叉点就在这。 - “lost unpark”:每线程只有一张许可,如果你的代码在条件检查和 park 之间隐式触发了别处的 park(javadoc 特别点名了类加载),许可可能被中途消费掉,所以锁实现类建议用 static 块提前加载 LockSupport。
- blocker 纯诊断用途,但线上排查时价值极大:无参 park() 在 jstack 里只有一行 parking,带 blocker 的能直接看到堵在哪个同步器上。