JDK 8 · 17 · 21 · ABSTRACTQUEUEDSYNCHRONIZER · STATE · CLH · PARK · CONDITION · REENTRANTLOCK · SEMAPHORE · COUNTDOWNLATCH
一个 int,一条排队的链
AQS 只管两样东西:一个叫 state 的 int,和一条先进先出的等待队列。“现在能不能拿到”由子类回答:tryAcquire / tryRelease 用 CAS 改 state。“拿不到怎么办”全由 AQS 包办:排队、park 睡下、谁来叫醒、超时和中断怎么撤。ReentrantLock、Semaphore、CountDownLatch、读写锁、线程池的 Worker、Tomcat 的连接数闸门,都是这样搭出来的。
假设场景面试被问“讲讲 AQS”,又在线上 jstack 里看到一大片 parking to wait for … ReentrantLock$NonfairSync,不知道是哪段代码在等。所以本页偏源码和排查,⑥ 末尾有面试题。版本按生产上最常见的 JDK 8、17、21 三个来讲:AQS 在 JDK 14 被重写过,所以 8 是一套写法,17 和 21 是另一套,面试资料里的 waitStatus、SIGNAL、acquireQueued 都是 JDK 8 的。标“实测”的在本机三个版本上都跑过:Temurin 8u512、17.0.21,Ubuntu OpenJDK 21.0.12,2 核 ARM 虚拟机。
①为什么需要:一次请求,路上就要过好几道 AQS
你可能从没直接写过 AQS,但一个普通的 Spring Boot 请求,从进 Tomcat 到写完日志,经过的这些地方全是它。下图里每一处都用 javap 和字节码扫描核过(版本见图下)。
Worker 就是一个 AQS 子类;SynchronousQueue 和 ConcurrentBag 是不用 AQS 的另一条路,③ 会拿它们对比。②没有它会怎样:自己写一把锁
最直接的写法是 CAS 自旋:抢不到就一直重试。另一个现成的选择是 synchronized。拿 8 个线程抢一把锁,锁里 sleep 1 ms(模拟拿着锁做 IO),三个 JDK 各跑一次:
| 8 个线程抢一把锁,跑 2 秒 | 每秒完成 | 2 秒里用掉的 CPU | 锁里只做一次自增时,每秒完成 |
|---|---|---|---|
| CAS 自旋锁 | 321~330 | 约 4.0 秒(两个核全满) | 490 万~680 万 |
synchronized | 934~935 | 0.05~0.06 秒 | 650 万~1160 万 |
ReentrantLock(AQS,默认非公平) | 932~934 | 0.06~0.10 秒 | 2340 万~3990 万 |
ReentrantLock(true)(公平) | 922~929 | 0.07~0.09 秒 | 20 万~32 万 |
范围是 JDK 8、17、21 三次的最小值到最大值,三个版本的结论一致。只有 2 核,同一时刻真正在跑的只有 2 个线程,所以自旋锁的“慢”主要是它抢走了持锁线程的 CPU:持锁的线程 sleep 醒来,要等别人转完时间片才轮到它。
拿不到就重试,等多久就烧多久。锁里一旦有 IO,两个核全花在“等”上,干活的反而被挤慢。要省 CPU,就得让等的线程睡下,还得有人在锁放开时叫醒它。这就需要一条记着“谁在等”的队列。
不能“等 1 秒拿不到就算了”,等的时候不能被中断,一把锁只有一个等待集合(wait / notify),不能选公平,不分读写。JDK 21 的虚拟线程在 synchronized 里阻塞还会钉住载体线程(见 ⑥ 坑 5)。
AQS 的精华:把“排队、睡下、叫醒、超时、中断”只写一遍,各种同步工具只需要回答一个问题:给定 state,现在能不能拿到、放掉以后要不要叫人。
不懂这一点的典型误用:以为 ReentrantLock 是“先来先得”的(默认是非公平的,刚放锁的线程几乎总能抢回来),或者以为 new ReentrantLock(true) 以后 tryLock() 也公平(不是),见 ⑥ 坑 3。
③相邻方案:等的线程放在哪
| 方案 | 等的线程放在哪 | 能做的 | 例子 |
|---|---|---|---|
synchronized | JVM 里的对象监视器(ObjectMonitor) | 互斥、可重入、一个 wait 集合;自动释放 | Hashtable、JDK 8 / 17 的 PrintStream |
| AQS 一族 | AQS 的等待队列,节点上挂着线程 | 独占和共享两种模式、超时、中断、多个 Condition、公平和非公平 | ReentrantLock、Semaphore、CountDownLatch、读写锁 |
StampedLock | 自己的一条等待队列,不是 AQS | 乐观读:读的时候不加锁,读完检查版本号 | 读多写极少、读的代码很短的地方 |
| 不用锁 | 没有等待;要等时用别的交接点 | CAS、原子类、无锁队列 | ConcurrentHashMap 的大部分路径、LongAdder、HikariCP 的 ConcurrentBag |
选择规则:只要互斥、没有特殊需求,synchronized 就行(JDK 21 虚拟线程里阻塞例外);要超时、可中断、多个条件、读写分离、限流计数,就用 AQS 一族;读多写极少才考虑 StampedLock。本页学 AQS。
④原理:state 说能不能拿,队列管拿不到怎么办
比方:银行一个窗口(state)加一排叫号椅(等待队列)。窗口空着就直接办;有人在办就取号坐下打盹;办完的人起身时拍一下下一位的肩膀。比方对不上的地方:默认情况下,刚走进来的人可以趁被拍的人还没站起来,直接冲到窗口前(非公平),见 为什么这样设计。
先知道这些
CAS
compareAndSetState(0, 1):是 0 才改成 1,一条指令完成,同时只有一个线程成功。之前的页面用过很多次。
park / unpark
LockSupport.park() 让线程睡下,unpark(t) 叫醒它。先 unpark 后 park 也行,会留一张“许可”。jstack 里的 WAITING (parking) 就是它。
CLH 队列
一种排队锁:每个等待者是链表上的一个节点,只盯着自己前面那个。AQS 用的是它的变体:节点双向链接,等待者 park 而不是一直转。
模板方法
父类写好流程,留几个方法给子类填。AQS 留的是 tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared、isHeldExclusively。
机制 1一个 state,一条队;节点上的状态,8 和 17 / 21 记法不同
state、head、tail。队列是懒建的:没人抢过,head 就是 null;第一次有人要排队时才建一个空的 head。拿到锁的线程,它的节点就成了新 head。两代实现的节点类也不同:JDK 8 是一个 Node 类,用 nextWaiter 区分独占和共享;JDK 17 / 21 拆成了 ExclusiveNode、SharedNode、ConditionNode。机制 2AQS 管排队,子类管 state 是什么意思
| 子类 | state 的意思 | 能不能拿(tryAcquire 系列) | 模式 |
|---|---|---|---|
ReentrantLock.Sync | 重入了几次,0 是没人拿 | 0 → CAS 成 1;已经是自己的就 +1 | 独占 |
ReentrantReadWriteLock.Sync | 高 16 位:读锁被拿了几次;低 16 位:写锁重入次数 | 写:整个是 0 才行;读:没有别人的写锁就 +1<<16 | 写独占、读共享 |
Semaphore.Sync | 剩几个许可 | 剩的 ≥ 要的,就 CAS 减掉 | 共享 |
CountDownLatch.Sync | 还要等几次 countDown | 只看是不是 0,不改 state | 共享 |
ThreadPoolExecutor.Worker | −1 还没启动,0 空闲,1 正在跑任务 | CAS 0 → 1,不可重入 | 独占 |
Tomcat LimitLatch.Sync | 不用 state,另用一个 AtomicLong 计数 | 计数 +1 后不超过上限才行,超了就减回去 | 共享 |
每一行都只写了“能不能拿”和“放掉后要不要叫人”。排队、park、被叫醒后重试、超时撤销,这些子类一行都不用写。
机制 3一次 lock / unlock 的全过程 A 拿着锁,B 来排队,A 放锁
开始。
机制 4共享模式:一个叫醒一个,一路传下去
机制 5Condition:另一条队,await 和 signal 在两条队之间搬节点
为什么这样设计:三个取舍,都有实测
被放弃的:拿不到就自旋
- 等多久烧多久,锁里有 IO 时两个核全满(② 实测约 4 秒 CPU / 2 秒)
- 还抢走了持锁线程的 CPU,干活的反而慢(每秒 321~330 次,对 930 次)
AQS:排队,然后 park
- 等的线程不占 CPU(0.06~0.10 秒)
- 代价:睡下、叫醒都是线程切换,微秒级。所以 JDK 17 / 21 里,排在最前面的线程被叫醒后又没抢到时,会先短暂自旋重试几次再睡
被放弃的:一个等待集合,notifyAll 全叫醒
- 50 个消费者等着,放一个元素叫醒一群,只有一个抢得到,其余白醒(实测平均每个元素叫醒 1.8~5.9 次)
AQS:按队列只叫醒一个
- release 只叫醒 head 后面那一个;Condition.signal 只挪一个(实测每个元素正好 1.0 次)
- 代价:被叫醒的人自己负责传给下一个(共享模式),逻辑更绕
被放弃的默认值:公平
- 每次交接都必须等一个睡着的线程醒来,锁里只做自增时每秒 20 万~32 万次,每次交接约 1 次线程切换
AQS 的默认:非公平
- 谁正在 CPU 上谁先拿,每秒 2340 万~3990 万次,约是公平锁的 100 倍
- 代价:排队的人会被插队,尾延迟变长(⑤ 实测 1000 次里 953~995 次被插队)
决定胜负的约束:线程切换是微秒级的,CAS 是纳秒级的。AQS 每个取舍都在躲线程切换:拿不到才睡、只叫醒一个、默认让正在跑的线程直接拿。和 ConcurrentBag、SynchronousQueue 是同一个账。
最少的 API,外加一个 8 行的自定义同步器
lock.lock();
try { … } finally { lock.unlock(); } // 一定放 finally
if (lock.tryLock(1, SECONDS)) { … } // 等 1 秒拿不到就算了
Condition notFull = lock.newCondition();
while (q.isFull()) notFull.await(); // 一定写 while
notFull.signal();
Semaphore sem = new Semaphore(10); // 最多 10 个并发
sem.acquire(); try { … } finally { sem.release(); }
CountDownLatch done = new CountDownLatch(3);
done.countDown(); done.await(5, SECONDS);
class BooleanLatch {
private static class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int ignore) {
return getState() != 0 ? 1 : -1; // 开了就放行
}
protected boolean tryReleaseShared(int ignore) {
setState(1); return true; // 打开,叫醒等的人
}
}
private final Sync sync = new Sync();
public void signal() { sync.releaseShared(1); }
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
}
读写锁的锁降级、lockInterruptibly、getQueueLength 这类监控方法、各版本差在哪,放到 第⑥步。
⑤亲眼看到:用反射把队列打出来
一个 Java 8 语法的小程序,javac --release 8 编一次,同一份 class 在 JDK 8、17、21 上跑:让 main 重入两次,三个线程来排队,再用反射读出 state、head 和每个节点的状态。最后测一下插队。
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
// 直接看 AQS 里面:state、排队的节点、节点状态
// 运行:JDK 8 直接 java AqsPeek;JDK 9+ 要加 --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED
public class AqsPeek {
static Object field(Object o, String... names) throws Exception {
for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass())
for (String n : names) try { Field f = c.getDeclaredField(n); f.setAccessible(true); return f.get(o); } catch (NoSuchFieldException e) { }
return null;
}
static AbstractQueuedSynchronizer sync(Object owner) throws Exception { return (AbstractQueuedSynchronizer) field(owner, "sync"); }
static String queue(AbstractQueuedSynchronizer s) throws Exception {
StringBuilder b = new StringBuilder("state=" + field(s, "state") + " 队列:");
Object n = field(s, "head");
if (n == null) return b.append("(还没建)").toString();
for (int i = 0; n != null && i < 10; i++, n = field(n, "next")) {
Thread t = (Thread) field(n, "waiter", "thread"); // JDK 14+ 叫 waiter,之前叫 thread
Object st = field(n, "status", "waitStatus"); // JDK 14+ 叫 status,之前叫 waitStatus
b.append(i == 0 ? "head" : " → ").append(i == 0 ? "" : (t == null ? "?" : t.getName())).append("[").append(n.getClass().getSimpleName()).append(" ").append(st).append("]");
}
return b.toString();
}
static void waitParked(Thread t) throws InterruptedException { while (t.getState() != Thread.State.WAITING && t.getState() != Thread.State.TIMED_WAITING) Thread.sleep(1); }
public static void main(String[] a) throws Exception {
System.out.println("JDK " + System.getProperty("java.version"));
final ReentrantLock lock = new ReentrantLock();
lock.lock(); lock.lock(); // main 重入两次
System.out.println("① main 重入 2 次:" + queue(sync(lock)) + "," + lock);
List<Thread> ws = new ArrayList<>();
for (String name : new String[] {"T1", "T2", "T3"}) {
Thread t = new Thread(() -> { lock.lock(); lock.unlock(); }, name); t.start(); waitParked(t); ws.add(t);
}
System.out.println("② 三个线程排队:" + queue(sync(lock)));
lock.unlock();
System.out.println("③ unlock 一次:" + queue(sync(lock)) + "(还剩 1 层,没人被叫醒)");
lock.unlock(); for (Thread t : ws) t.join();
System.out.println("④ 全部走完: " + queue(sync(lock)));
Semaphore sem = new Semaphore(3); sem.acquire(2);
CountDownLatch latch = new CountDownLatch(2); latch.countDown();
ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); rw.readLock().lock(); rw.readLock().lock(); rw.writeLock();
System.out.println("⑤ Semaphore(3) 拿走 2 个:state=" + field(sync(sem), "state") + ";CountDownLatch(2) countDown 一次:state=" + field(sync(latch), "state")
+ ";读锁拿两次:state=" + field(sync(rw), "state") + "(高 16 位是读锁数," + ((int) field(sync(rw), "state") >>> 16) + ")");
// ⑥ W 已经在排队,持锁的线程 unlock 后马上又抢:tryLock() 和 lock() 分别谁拿到?
for (boolean fair : new boolean[] {false, true}) {
int tryWins = 0, lockWins = 0;
for (int i = 0; i < 1000; i++) {
final ReentrantLock l = new ReentrantLock(fair); l.lock();
Thread w = new Thread(() -> { l.lock(); l.unlock(); }); w.start(); waitParked(w);
l.unlock();
if (l.tryLock()) { tryWins++; l.unlock(); }
w.join();
final ReentrantLock l2 = new ReentrantLock(fair); l2.lock();
final java.util.concurrent.atomic.AtomicBoolean wGot = new java.util.concurrent.atomic.AtomicBoolean();
Thread w2 = new Thread(() -> { l2.lock(); wGot.set(true); l2.unlock(); }); w2.start(); waitParked(w2);
l2.unlock();
l2.lock(); if (!wGot.get()) lockWins++; l2.unlock();
w2.join();
}
System.out.println("⑥ " + (fair ? "公平 " : "非公平") + ":放下马上再抢,1000 次里 tryLock() 抢到 " + tryWins + " 次,lock() 抢在 W 前面 " + lockWins + " 次");
}
}
}
$ java AqsPeek # JDK 1.8.0_512
② 三个线程排队:state=2 队列:head[Node -1] → T1[Node -1] → T2[Node -1] → T3[Node 0]
⑥ 非公平:放下马上再抢,1000 次里 tryLock() 抢到 999 次,lock() 抢在 W 前面 995 次
⑥ 公平 :放下马上再抢,1000 次里 tryLock() 抢到 999 次,lock() 抢在 W 前面 0 次
$ java --add-opens … AqsPeek # JDK 17.0.21
② 三个线程排队:state=2 队列:head[ExclusiveNode 0] → T1[ExclusiveNode 1] → T2[ExclusiveNode 1] → T3[ExclusiveNode 1]
⑥ 非公平:放下马上再抢,1000 次里 tryLock() 抢到 1000 次,lock() 抢在 W 前面 953 次
⑥ 公平 :放下马上再抢,1000 次里 tryLock() 抢到 1000 次,lock() 抢在 W 前面 0 次
$ java --add-opens … AqsPeek # JDK 21.0.12
② 三个线程排队:state=2 队列:head[ExclusiveNode 0] → T1[ExclusiveNode 1] → T2[ExclusiveNode 1] → T3[ExclusiveNode 1]
⑥ 非公平:放下马上再抢,1000 次里 tryLock() 抢到 1000 次,lock() 抢在 W 前面 960 次
⑥ 公平 :放下马上再抢,1000 次里 tryLock() 抢到 1000 次,lock() 抢在 W 前面 0 次
# 三个版本都一样的几行
① main 重入 2 次:state=2 队列:(还没建),java.util.concurrent.locks.ReentrantLock@…[Locked by thread main]
③ unlock 一次:state=1 队列:(同上,没人被叫醒)
④ 全部走完: state=0 队列:head[…]
⑤ Semaphore(3) 拿走 2 个:state=1;CountDownLatch(2) countDown 一次:state=1;读锁拿两次:state=131072(高 16 位是读锁数,2)
| 看到了什么 | 对应 ④ |
|---|---|
| 没人抢时队列根本不存在;排队后 head 是一个不挂线程的空节点 | 机制 1 |
| JDK 8 的 -1 记在前驱上(T3 自己是 0),JDK 17 / 21 的 1 记在自己身上(head 是 0) | 机制 1 |
| 重入两次只 unlock 一次,state 还是 1,排队的人一个都没被叫醒 | 机制 2 |
| 同一个 state,Semaphore 存许可数,CountDownLatch 存剩余计数,读写锁的读锁记在高 16 位(2 × 65536 = 131072) | 机制 2 |
| 非公平锁刚放下就再抢,1000 次里 953~995 次抢在排队的人前面;公平锁的 lock() 一次也没有,可 tryLock() 照样插队 | 为什么这样设计 |
⑥细节:坑、两代源码、日常开发里的完整清单
每条坑都在 JDK 8、17、21 上跑过;源码一段 JDK 8、一段 JDK 17 / 21,对照着读。
注意事项
- 现象
- 某个分支 return 或抛异常没走到 unlock,之后所有线程都停在
parking to wait for … NonfairSync。如果持锁的线程已经因为异常死掉了,jstack 里连持有者都找不到。 - 根源
- synchronized 的释放由 JVM 保证(字节码里有异常表),AQS 的 state 只是一个 int,没人调
tryRelease它就永远不是 0。owner 字段还指着那个线程。 - 做法
lock()写在 try 外面紧挨着 try,unlock()写在 finally 第一行。
- 现象
- ⑤ 实测:重入两次只 unlock 一次,state 还是 1,排队的三个线程一个都没被叫醒。没拿锁就 unlock,三个版本都抛
IllegalMonitorStateException。实测 - 根源
- tryRelease 只把 state 减 1,减到 0 才算释放、才叫人。
- 做法
- 一次 lock 对应一个 finally 里的 unlock;排查时看
getHoldCount()。
- 现象
- 非公平锁刚放下马上又抢,1000 次里 953~995 次抢在排队的人前面;
new ReentrantLock(true)的 lock() 一次都没插队,可 tryLock() 1000 次里抢到 999~1000 次。实测 - 根源
- 非公平的 tryAcquire 不看队列,state 是 0 就 CAS;公平的先看
hasQueuedPredecessors()。而tryLock()调的是 Sync 里那个不看队列的版本,javadoc 写明了它会“打破公平”。 - 做法
- 要严格先来先得:公平锁 +
lock()或tryLock(0, TimeUnit.SECONDS)(带超时的版本遵守公平)。多数场景用默认非公平,接受尾延迟。
- 现象
- 队列空时消费者 await,被 signal 醒来直接 poll,偶尔拿到 null 或越界。
- 根源
- signal 只是把节点挪到同步队列,醒来还要重新抢锁;抢到之前,别的线程可能已经把东西拿走了。另外还有虚假唤醒。
- 做法
while (条件不满足) cond.await();,ArrayBlockingQueue 的源码就是这么写的。
- 现象
- 100 个虚拟线程各拿各的锁、锁里 sleep 50 ms:用 synchronized 要 2517 ms(2 个载体线程一个个轮),换成 ReentrantLock 只要 66 ms。加
-Djdk.tracePinnedThreads=short会打印<== monitors:1。实测 JDK 21 - 根源
- AQS 的等待是
LockSupport.park,虚拟线程 park 时会从载体线程上卸下来;JDK 21 的 synchronized 做不到,阻塞时连载体线程一起占住。JDK 24(JEP 491)修了,但 21 没有。JDK 8、17 没有虚拟线程,不涉及。 - 做法
- JDK 21 用虚拟线程时,包着 IO 的锁换成 ReentrantLock。JDK 21 自己也这么做了:java.io 的 PrintStream、BufferedOutputStream 等改用了内部的 ReentrantLock。
- 现象
- synchronized 的持有者在 jstack 里显示
- locked <0x…>;ReentrantLock 的持有者默认什么也不显示。 - 根源
- AQS 的锁不是 JVM 监视器,只有加
-l,线程下面才会多出Locked ownable synchronizers: - <0x…> (a …ReentrantLock$NonfairSync)。 - 做法
jstack -l <pid>,拿等待者的parking to wait for <0x…>地址去搜,同一地址出现在 ownable synchronizers 下的线程就是持有者。三个版本实测都这样;JDK 8 等待者的栈帧是acquireQueued,17 / 21 是acquire。
- 现象
- 先
readLock().lock(),再writeLock().tryLock(1 秒):返回 false;换成lock()就永远卡住。反过来,拿着写锁再拿读锁(锁降级)没问题。实测 - 根源
- 写锁要求 state 的高 16 位(读锁数)为 0,可这里面就有自己那一份读锁。
- 做法
- 要写就先放掉读锁再拿写锁,拿到后重新检查数据;或者干脆一开始就拿写锁。
- 现象
- ① JDK 17.0.11 之前:Condition 上等的线程很多时,CPU 偏高(JDK-8317960,只有 17u 有这个问题)。② JDK 17.0.13、21.0.5 之前:被取消的 ConditionNode 能熬过 Minor GC,多占内存(JDK-8325754)。③ 大量线程用很短的超时去
tryAcquire、又拿不到时,cleanQueue可能活锁(JDK-8386085)。 - 根源
- 都是 JDK 14 重写后新代码里的细节:① 醒来判断“自己进同步队列没有”时从队尾遍历;② 取消的节点没及时断链;③ 清理队列时多做了一次 CAS。
- 做法
- 17 至少 17.0.13,21 至少 21.0.5。③ 主线 2026-06 修复,我查看时只回移到了 25u-dev,8、17、21 还没有;JDK 8 的老实现没有这三个问题。
使用规范
Semaphore 的 acquire / release、读写锁同理。
锁永远不放(坑 1)
tryLock(timeout)、await(timeout)、CountDownLatch.await(timeout),并处理 false。
下游卡住时整条链路的线程全挂死
生产者等 notFull、消费者等 notEmpty,各 signal 各的。
拿到不该拿的、叫醒不该醒的(坑 4)
实测 Semaphore(2) 拿 1 次放 2 次后,许可变成 3:它不检查谁放的。
限流失效,并发越放越多
JDK 8 / 17 照常用 synchronized 即可。
载体线程被钉住,吞吐塌掉(坑 5)
同时看 parking to wait for 和 Locked ownable synchronizers。
找不到持有者(坑 6)
读源码:同一件事,两代写法
| 做的事 | JDK 8(也是 9~13) | JDK 17 / 21(JDK 14 重写) |
|---|---|---|
| 节点 | 一个 Node:waitStatus、prev、next、thread、nextWaiter | ExclusiveNode / SharedNode / ConditionNode:status、prev、next、waiter |
| 状态值 | SIGNAL = -1(记在前驱上)、CANCELLED = 1、CONDITION = -2、PROPAGATE = -3 | WAITING = 1(记在自己身上)、CANCELLED 为负数、COND = 2,没有 PROPAGATE |
| 入队 | addWaiter 快速 CAS tail,失败走 enq 自旋;第一次入队时建空 head | 都在 acquire(node, arg, shared, interruptible, timed, time) 这一个方法的循环里 |
| 排队等 | acquireQueued:前驱是 head 就 tryAcquire;否则 shouldParkAfterFailedAcquire 把前驱改成 SIGNAL,再 park | 同一个 acquire 循环:是第一个就 tryAcquire;先把自己的 status 设成 WAITING 再查一次,然后 park;被叫醒后先自旋几次 |
| 叫醒 | release → unparkSuccessor(head),后继为空或已取消时从 tail 往前找 | release → signalNext(head),只看 head.next;取消的节点由 cleanQueue 摘掉 |
| 共享模式 | setHeadAndPropagate + doReleaseShared + PROPAGATE | signalNextIfShared |
| jstack 栈帧 | AbstractQueuedSynchronizer.acquireQueued(实测) | AbstractQueuedSynchronizer.acquire(实测) |
JDK 8:acquire → addWaiter → acquireQueued
1197 public final void acquire(int arg) {1198 if (!tryAcquire(arg) &&❶1199 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))❷1200 selfInterrupt();1201 }⋮605 private Node addWaiter(Node mode) {606 Node node = new Node(Thread.currentThread(), mode);607 // Try the fast path of enq; backup to full enq on failure608 Node pred = tail;609 if (pred != null) {❸610 node.prev = pred;611 if (compareAndSetTail(pred, node)) {❹612 pred.next = node;613 return node;614 }615 }616 enq(node);❺617 return node;618 }⋮583 private Node enq(final Node node) {584 for (;;) {585 Node t = tail;586 if (t == null) { // Must initialize❻587 if (compareAndSetHead(new Node()))❼588 tail = head;589 } else {590 node.prev = t;591 if (compareAndSetTail(t, node)) {592 t.next = node;593 return t;594 }595 }596 }597 }
857 final boolean acquireQueued(final Node node, int arg) {858 boolean failed = true;859 try {860 boolean interrupted = false;861 for (;;) {862 final Node p = node.predecessor();863 if (p == head && tryAcquire(arg)) {❽864 setHead(node);❾865 p.next = null; // help GC866 failed = false;867 return interrupted;868 }869 if (shouldParkAfterFailedAcquire(p, node) &&❿870 parkAndCheckInterrupt())871 interrupted = true;872 }873 } finally {874 if (failed)875 cancelAcquire(node);876 }877 }⋮795 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {796 int ws = pred.waitStatus;797 if (ws == Node.SIGNAL)undefined798 /*799 * This node has already set status asking a release800 * to signal it, so it can safely park.801 */802 return true;803 if (ws > 0) {undefined804 /*805 * Predecessor was cancelled. Skip over predecessors and806 * indicate retry.807 */808 do {809 node.prev = pred = pred.prev;810 } while (pred.waitStatus > 0);811 pred.next = node;812 } else {813 /*814 * waitStatus must be 0 or PROPAGATE. Indicate that we815 * need a signal, but don't park yet. Caller will need to816 * retry to make sure it cannot acquire before parking.817 */818 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);undefined819 }820 return false;821 }
- 1先试一次 tryAcquire(子类写的),成了就走,根本不碰队列。
- 2没成:addWaiter 入队,acquireQueued 排队等;等的过程中被中断过,最后补一次
selfInterrupt()(lock() 不响应中断,只是记下来)。 - 3快速路径:队列已经有了,直接把新节点 CAS 成 tail。
- 4注意顺序:先设
node.prev,CAS tail 成功后才设pred.next。所以从前往后的 next 可能暂时是断的,从后往前的 prev 一定是通的。这就是 unparkSuccessor 要从 tail 往前找的原因。 - 5快速路径失败(队列还没建,或者 CAS 被别人抢了)走 enq 自旋。
- 6队列为空:先放一个空节点当 head。
- 7head 和 tail 都指向这个空节点,下一圈循环再把自己挂上去。
- 8只有前驱是 head(自己排第一)才有资格 tryAcquire。
- 9拿到了:自己成为新 head,清掉 thread 字段,原来的 head 断开等 GC。
- 10没拿到:问 shouldParkAfterFailedAcquire 能不能睡,能就 park,醒来接着循环。
- 11前驱已经是 SIGNAL:它放锁时会叫我,放心睡。
- 12前驱被取消了(> 0):往前跳过所有取消的节点,重新挂好。
- 13否则把前驱 CAS 成 SIGNAL,这次先不睡,回去再试一次 tryAcquire。所以一个线程至少试两次才会睡。
JDK 17 / 21:一个 acquire 方法做完全部
704 final int acquire(Node node, int arg, boolean shared,705 boolean interruptible, boolean timed, long time) {706 Thread current = Thread.currentThread();707 byte spins = 0, postSpins = 0; // retries upon unpark of first thread708 boolean interrupted = false, first = false;709 Node pred = null; // predecessor of node when enqueued710711 /*712 * Repeatedly:713 * Check if node now first714 * if so, ensure head stable, else ensure valid predecessor715 * if node is first or not yet enqueued, try acquiring716 * else if queue is not initialized, do so by attaching new header node717 * resort to spinwait on OOME trying to create node718 * else if node not yet created, create it719 * resort to spinwait on OOME trying to create node720 * else if not yet enqueued, try once to enqueue721 * else if woken from park, retry (up to postSpins times)722 * else if WAITING status not set, set and retry723 * else park and clear WAITING status, and check cancellation724 */725726 for (;;) {727 if (!first && (pred = (node == null) ? null : node.prev) != null &&❶728 !(first = (head == pred))) {729 if (pred.status < 0) {❷730 cleanQueue(); // predecessor cancelled731 continue;732 } else if (pred.prev == null) {733 Thread.onSpinWait(); // ensure serialization734 continue;735 }736 }737 if (first || pred == null) {❸738 boolean acquired;739 try {740 if (shared)741 acquired = (tryAcquireShared(arg) >= 0);742 else743 acquired = tryAcquire(arg);744 } catch (Throwable ex) {745 cancelAcquire(node, interrupted, false);746 throw ex;747 }748 if (acquired) {❹749 if (first) {❺750 node.prev = null;751 head = node;752 pred.next = null;753 node.waiter = null;754 if (shared)755 signalNextIfShared(node);756 if (interrupted)757 current.interrupt();758 }759 return 1;760 }761 }762 Node t;763 if ((t = tail) == null) { // initialize queue❻764 if (tryInitializeHead() == null)765 return acquireOnOOME(shared, arg);766 } else if (node == null) { // allocate; retry before enqueue767 try {768 node = (shared) ? new SharedNode() : new ExclusiveNode();769 } catch (OutOfMemoryError oome) {770 return acquireOnOOME(shared, arg);771 }772 } else if (pred == null) { // try to enqueue❼773 node.waiter = current;774 node.setPrevRelaxed(t); // avoid unnecessary fence775 if (!casTail(t, node))❽776 node.setPrevRelaxed(null); // back out777 else778 t.next = node;779 } else if (first && spins != 0) {❾780 --spins; // reduce unfairness on rewaits781 Thread.onSpinWait();782 } else if (node.status == 0) {❿783 node.status = WAITING; // enable signal and recheck784 } else {785 long nanos;786 spins = postSpins = (byte)((postSpins << 1) | 1);undefined787 if (!timed)788 LockSupport.park(this);undefined789 else if ((nanos = time - System.nanoTime()) > 0L)790 LockSupport.parkNanos(this, nanos);791 else792 break;793 node.clearStatus();794 if ((interrupted |= Thread.interrupted()) && interruptible)undefined795 break;796 }797 }798 return cancelAcquire(node, interrupted, interruptible);undefined799 }
- 1先看自己是不是排第一(前驱是 head)。
first一旦为 true 就不再重算。 - 2前驱被取消了:调 cleanQueue 把取消的节点摘掉,再来。
- 3排第一,或者还没入队:都可以试 tryAcquire / tryAcquireShared。还没入队就先试一次,和 JDK 8 的“先试再排”一样。
- 4拿到了。
- 5如果是排队拿到的:自己成为 head,断开旧 head;共享模式顺手叫醒下一个共享节点。
- 6队列还没建:建一个空 head。
- 7节点还没建:现在才 new。先试、后建节点,没竞争时一个对象都不分配。
- 8CAS tail 入队;和 JDK 8 一样先设 prev 再设 next。
- 9被叫醒后又没抢到:先
onSpinWait自旋几次,减少“刚醒就被插队、又得睡”的次数。 - 10第一次走到这里:把自己的 status 设成 WAITING,不睡,回去再查一次。对应 JDK 8 把前驱设成 SIGNAL。
- 11真要睡了:自旋次数按 1、3、7… 翻倍,下次醒来没抢到时多转一会儿。
- 12park;限时版本用 parkNanos,时间到了就跳出循环。
- 13醒来清掉自己的 status;可中断的版本发现被中断就退出。
- 14超时或中断:cancelAcquire 把节点标成 CANCELLED 并清理队列。
1092 public final boolean release(int arg) {1093 if (tryRelease(arg)) {1094 signalNext(head);❶1095 return true;1096 }1097 return false;1098 }⋮641 private static void signalNext(Node h) {642 Node s;643 if (h != null && (s = h.next) != null && s.status != 0) {❷644 s.getAndUnsetStatus(WAITING);645 LockSupport.unpark(s.waiter);❸646 }647 }
638 private void unparkSuccessor(Node node) {639 /*640 * If status is negative (i.e., possibly needing signal) try641 * to clear in anticipation of signalling. It is OK if this642 * fails or if status is changed by waiting thread.643 */644 int ws = node.waitStatus;645 if (ws < 0)❹646 compareAndSetWaitStatus(node, ws, 0);647648 /*649 * Thread to unpark is held in successor, which is normally650 * just the next node. But if cancelled or apparently null,651 * traverse backwards from tail to find the actual652 * non-cancelled successor.653 */654 Node s = node.next;655 if (s == null || s.waitStatus > 0) {❺656 s = null;657 for (Node t = tail; t != null && t != node; t = t.prev)❻658 if (t.waitStatus <= 0)659 s = t;660 }661 if (s != null)662 LockSupport.unpark(s.thread);663 }
- 1tryRelease 返回 true(state 真的放干净了)才叫人。
- 2只看 head 后面那一个,而且它的 status 不是 0(真在睡)才叫。
- 3先清掉 WAITING 再 unpark,避免重复叫。
- 4JDK 8:先把 head 的 SIGNAL 清成 0。
- 5后继为空或已取消:
- 6从 tail 往前找最靠前的、没被取消的节点。原因见上面 addWaiter 第 4 条:prev 链一定是完整的。
205 final void lock() {206 if (compareAndSetState(0, 1))❶207 setExclusiveOwnerThread(Thread.currentThread());208 else209 acquire(1);210 }⋮231 protected final boolean tryAcquire(int acquires) {232 final Thread current = Thread.currentThread();233 int c = getState();234 if (c == 0) {235 if (!hasQueuedPredecessors() &&❷236 compareAndSetState(0, acquires)) {237 setExclusiveOwnerThread(current);238 return true;239 }240 }
223 final boolean initialTryLock() {224 Thread current = Thread.currentThread();225 if (compareAndSetState(0, 1)) { // first attempt is unguarded❶226 setExclusiveOwnerThread(current);227 return true;⋮279 protected final boolean tryAcquire(int acquires) {280 if (getState() == 0 && !hasQueuedPredecessors() &&❷281 compareAndSetState(0, acquires)) {282 setExclusiveOwnerThread(Thread.currentThread());283 return true;
- 1非公平:lock() 一上来就 CAS,不管有没有人排队。插队就发生在这一行。
- 2公平:tryAcquire 先问
hasQueuedPredecessors(),有人排在前面就不抢。两代写法不同,规则一样。
实验台:自己点一遍排队、插队、条件队列 先猜,再一步步点
一次点击是一个线程做完一个动作(lock、unlock、醒来重试、await、signal、countDown、超时)。规则照 JDK 21 的源码写,CAS 在这里总是一次完成。右上角可以切换节点状态的显示方式:JDK 17 / 21 记在自己身上,JDK 8 记在前驱上。
同一套模拟逻辑在 node 里随机测过:独占锁(公平、非公平)、Semaphore、CountDownLatch 各 2 万组、共约 165 万步随机操作,没有一次出现两个持有者,也没有一次“资源空着、却有人睡着没人叫”。
日常开发里的 AQS:完整清单
方法:写了一个读 class 文件常量池的小程序,扫 JDK 的 java.base(JDK 8 是 rt.jar)和框架 jar,找出直接继承 AQS 的类、以及引用了 ReentrantLock 等同步器的类,再用 javap 看关键方法里是不是真的在调。
| JDK 8u512 | JDK 17.0.21 | JDK 21.0.12 | |
|---|---|---|---|
| 直接继承 AQS 的类 | 三个版本都是同样 5 个:ReentrantLock.Sync、ReentrantReadWriteLock.Sync、Semaphore.Sync、CountDownLatch.Sync、ThreadPoolExecutor.Worker | ||
| 引用 ReentrantLock 的类 | 28 个(整个 rt.jar),几乎都在 java.util.concurrent | 58 个(java.base) | 66 个(java.base) |
| java.util.concurrent | 一直是大头:ArrayBlockingQueue、LinkedBlockingQueue(两把锁)、LinkedBlockingDeque、PriorityBlockingQueue、DelayQueue、CyclicBarrier、ScheduledThreadPoolExecutor 的延迟队列。线程池的队列全在这里 | ||
| 网络、TLS | synchronized | NioSocketImpl(普通 Socket 的读写)、SocketChannelImpl、SSLSocketImpl 等 20 个 TLS 类、HttpURLConnection、KeepAliveCache | |
| java.io | synchronized(jstack 里是 waiting to lock … (a java.io.PrintStream),实测) | PrintStream、BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter、InputStreamReader、OutputStreamWriter 等 11 个类改用 InternalLock(里面是 ReentrantLock,JDK 19 为虚拟线程加的;只在类正好是 JDK 自己的、不是子类时用;-Djdk.io.useMonitors=true 可退回)。jstack 里两个线程抢 println 变成 parking to wait for … ReentrantLock$NonfairSync(实测) | |
| 框架 | Spring Boot 2.7.18(JDK 8 能用的最后一代) | Spring Boot 3.5.16(JDK 17 / 21) |
|---|---|---|
| Tomcat | 9.0.83 和 10.1.55 一样:LimitLatch.Sync 继承 AQS,Acceptor 收连接前 countUpOrAwait(),超过 maxConnections(默认 8192)就停在这里;自家线程池的 Worker 也继承 AQS;TaskQueue 继承 LinkedBlockingQueue;SemaphoreValve 用 Semaphore 限并发 | |
| Spring MVC | 5.3.31 和 6.2.19 一样:AbstractHandlerMethodMapping.getHandlerInternal 每个请求都 acquireReadLock()(ReentrantReadWriteLock 的读锁),注册映射时拿写锁;ConcurrentReferenceHashMap.Segment 直接继承 ReentrantLock | |
| Spring 容器 | 5.3:创建单例 bean 用 synchronized | 6.2:DefaultSingletonBeanRegistry 改成 singletonLock(Lock)+ lenientCreationLock 和一个 Condition |
| logback | 1.2.12 和 1.5.34 一样:OutputStreamAppender.writeBytes 每写一条日志都 lock() / unlock(),并发打日志时线程排在这里 | |
| HikariCP | 4.0.3 和 6.3.3 一样:借还连接不走 AQS;只有 allowPoolSuspension=true 时才建一个 Semaphore(10000, true) 做暂停闸门,否则是空实现 FAUX_LOCK | |
| Kafka 客户端 | 3.1.2 和 3.9.2:生产者 BufferPool 用 ReentrantLock + Condition,缓冲区满时 send() 停在 await(max.block.ms);ProduceRequestResult 用 CountDownLatch,future.get() 就等在它上面 | |
版本差异:JDK 8 / 17 / 21
| JDK 8 | JDK 17 | JDK 21 | |
|---|---|---|---|
| AQS 实现 | 老实现(waitStatus、acquireQueued) | JDK 14 重写后的新实现(JDK-8229442):status、单个 acquire、signalNext、cleanQueue | |
| 被叫醒后自旋 | 没有,没抢到就再睡 | 排第一的线程被叫醒又没抢到时,自旋 1、3、7… 次再睡 | |
| Condition 等待 | 普通 park | ConditionNode 实现 ForkJoinPool.ManagedBlocker:在 ForkJoinPool 里 await 时,池子能补一个线程 | |
| 该升到的小版本 | 无相关问题 | 至少 17.0.13(8317960 在 17.0.11 修,8325754 在 17.0.13 修) | 至少 21.0.5(8325754) |
| 虚拟线程 | 没有 | 有;AQS 的 park 会卸下虚拟线程,synchronized 会钉住(到 JDK 24 才修) | |
| JDK 自己用 AQS 的地方 | 基本只有 java.util.concurrent | 多了网络、TLS、HTTP | 再多了 java.io(PrintStream 等)、InetAddress 缓存、ReferenceQueue |
| 实测:锁里只自增,8 线程 | 非公平 3990 万/秒,公平 32 万/秒 | 3620 万 / 20 万 | 2340 万 / 23 万 |
8317960 带着 [17u] 标记,只修在 17u 上;21u 的源码里 canReacquire 已经是修复后的写法(先看 p.next == node,看不出来才遍历)。各修复属于哪个小版本,是拿 GitHub 上 jdk17u、jdk21u 的提交和 jdk-17.0.x-ga、jdk-21.0.x-ga 标签比出来的。吞吐数字只在这台 2 核机器上有意义,看相对大小。
再往深一点
为什么要有一个空的 head
- “排第一”就简单定义成“前驱是 head”,不用特殊处理空队列。
- 拿到锁的线程,它的节点直接变成 head,清掉线程引用:head 永远代表“当前持有者或者刚放手的那个位置”。
- 队列懒建:从来没人竞争的锁,连这个空节点都不会分配(⑤ 里第 ① 行“还没建”)。
JDK 8 为什么把 SIGNAL 记在前驱上
- 放锁的线程只看 head 一个节点的 waitStatus 就知道“要不要叫人”:是 0 说明后面没人睡,连 unparkSuccessor 都不用进。
- 代价是理解起来绕:一个节点的状态描述的是它后面那个。JDK 14 改成节点只描述自己,再用“head.next 的 status 不为 0 才叫”达到同样效果,面试时两种都要说得出。
读写锁的 state 怎么切
- 高 16 位是所有线程读锁的总次数,低 16 位是写锁重入次数,各自最多 65535。
- 每个线程自己重入了几次读锁,另外记在 ThreadLocal 的
HoldCounter里,还缓存了第一个读线程和最近一个读线程,省掉多数 ThreadLocal 查找。 - 写锁要求高低 16 位都为 0(除了自己的写锁重入),所以拿着读锁升级会自己等自己(坑 7)。
Worker、LimitLatch:什么时候值得自己继承 AQS
- 线程池的
Worker要一把不可重入的锁来区分“空闲”和“在跑任务”,见 ThreadPoolExecutor 那页。 - Tomcat 的
LimitLatch要“计数到上限就排队”,而且上限可以运行中用setLimit改;它把计数放在一个 AtomicLong 里,只借用 AQS 的共享模式来排队和叫醒。 - 共同点:现成的锁、信号量、门闩语义不对,又需要排队和超时,才值得自己写。业务代码里几乎用不到。
面试官会怎么问
讲讲 AQS 的原理。
一个 volatile int state 加一条 FIFO 的双向等待队列(CLH 的变体)。子类实现 tryAcquire / tryRelease(或共享版本)来定义 state 的含义和能不能拿到;拿不到时 AQS 负责把线程包成节点入队、park,释放时叫醒队首的后继,被叫醒的线程重新 tryAcquire。还支持超时、中断、Condition。JDK 8 里用 waitStatus 的 SIGNAL 记在前驱上表示“要叫醒后继”,JDK 14 起改成节点自己的 status。
公平锁和非公平锁的区别?为什么默认非公平?
区别只在 tryAcquire:公平锁先看 hasQueuedPredecessors,有人排队就不抢;非公平锁 state 为 0 就直接 CAS。默认非公平是因为交接给一个睡着的线程要等它醒来,这段时间锁是空的,让正在运行的线程直接拿走,吞吐高得多(本机实测约 100 倍)。代价是排队的线程可能被反复插队。另外 tryLock() 在公平锁上也不公平。
JDK 8 的 unparkSuccessor 为什么要从 tail 往前找?
入队时先设 node.prev,CAS tail 成功后才设 pred.next,next 指针有一段时间是断的;节点取消时 next 也可能被改。prev 链总是完整的,所以后继为空或被取消时,从 tail 往前找最靠前的有效节点。
CountDownLatch 和 CyclicBarrier、Semaphore 怎么实现的?
CountDownLatch 和 Semaphore 都是 AQS 共享模式:Latch 的 state 是剩余计数,归零时 releaseShared,等待者一个叫醒一个;Semaphore 的 state 是许可数,acquire 减、release 加。CyclicBarrier 不直接继承 AQS,它用一把 ReentrantLock 加一个 Condition(trip)实现,最后一个到达的线程 signalAll。
Condition 和 Object.wait/notify 有什么区别?
Condition 依附于 AQS 的锁,一把锁可以建多个 Condition,各自一条等待队列,可以精确地只叫醒等某个条件的线程;支持超时和不可中断的等待。await 时节点进条件队列并完全释放锁,signal 把节点转到同步队列,等锁释放后才真正醒来重新抢锁。
下一圈
- 已做:AQS 下一圈:读写锁、StampedLock、Semaphore 与 JFR 找锁竞争;synchronized 的实现见 synchronized 与锁升级、CAS 与原子类
synchronized的实现:对象头、轻量级锁、ObjectMonitor 的 EntryList 和 cxq,JDK 15 起默认关闭偏向锁StampedLock:乐观读的版本号,以及它自己的那条等待队列ReentrantReadWriteLock的写锁饥饿和readerShouldBlockFutureTask:JDK 6 用 AQS,JDK 7 起换成自己的等待栈,为什么- JEP 491:JDK 24 怎么让 synchronized 不再钉住虚拟线程
- 用 JFR 的
jdk.JavaMonitorEnter和jdk.ThreadPark事件找锁竞争
↺回到第⑤步:改 AqsPeek
每个练习都写了做对时会看到什么。
练习 1 · 把公平锁的插队堵上
在 AqsPeek 第 ⑥ 段公平锁那次,给 W 加一个“我拿到了”的 AtomicBoolean,只数 main 抢在 W 前面的次数;再把 l.tryLock() 换成 l.tryLock(0, TimeUnit.SECONDS) 对比。
做对了会看到:本机三个版本,tryLock() 抢在 W 前面 891~991 次,tryLock(0, SECONDS) 0 次:带超时的版本走 tryAcquireNanos → 公平的 tryAcquire,会先看有没有人排在前面。不加标志的话会多数进去“W 已经拿完走了”的那几次。
用到:坑 3
练习 2 · 自己写一个 AQS 子类
照 ④ 的 BooleanLatch,写一个最多 N 个线程同时进入的“闸门”(共享模式,state 存剩余名额,tryAcquireShared 里 CAS 减),用 10 个线程测。
做对了会看到:任意时刻最多 N 个线程在闸门里;用 ⑤ 的反射打印队列,JDK 17 / 21 里等着的是 SharedNode,JDK 8 里是 Node,waitStatus 也是 -1。
用到:④ 机制 2、机制 4
练习 3 · 用 jstack 找持有者
让一个线程 lock 后 sleep 60 秒,另一个线程去 lock,然后分别执行 jstack 和 jstack -l。
做对了会看到:不加 -l 只看得到等待者的 parking to wait for <0x…>;加 -l 后,持有者下面出现同一个地址的 Locked ownable synchronizers。JDK 8 等待者栈帧是 acquireQueued,17 / 21 是 acquire。
用到:坑 6
1. 用一句话说出 AQS 的精华。
把排队、睡下、叫醒、超时、中断写一遍,子类只用 tryAcquire / tryRelease 说清 state 怎么算“拿到”和“放掉”。
← ② 精华
2. 自己用 CAS 自旋写锁,痛在哪?
等的线程一直在烧 CPU,锁里有 IO 时两个核全满,还抢走了持锁线程的时间片;想不烧 CPU 就得有队列记住谁在等、park 和 unpark,这正是 AQS 做的。
← ② 实测
3. “ReentrantLock 锁住了一直不放”,最常见的根源是 ④ 里哪一条?
state 只是一个 int,只有持有者调 tryRelease 才会变回 0;没在 finally 里 unlock,或者重入次数和 unlock 次数不对,state 就永远不是 0。
← ④ 机制 2
4. 预测:Semaphore(1) 当互斥锁用,A 拿着,B 在等,A 的代码里 release 了两次。之后 B 和新来的 C 会怎样?
许可变成 2(Semaphore 不检查谁放的),B 被叫醒拿走 1 个,C 也能直接拿走另 1 个,两个线程同时进了“互斥区”。
← ④ 机制 2 + 规范第 4 条
5. 预测:一个线程 lock() 等锁时被 interrupt() 了,它会怎样?换成 lockInterruptibly() 呢?
lock():不理会,继续排队,拿到锁后把中断标志补回去(JDK 8 的 selfInterrupt,17 / 21 在 acquire 里重设)。lockInterruptibly():醒来发现被中断,取消节点,抛 InterruptedException。
← ⑥ 源码 JDK 8 第 2 条、JDK 21 第 13~14 条
6. 预测:CountDownLatch(1) 上有 100 个线程在 await,countDown 之后多久全部醒来?会不会一下子 100 个线程同时抢 CPU?
不会同时。countDown 只叫醒第一个,第一个通过后叫醒第二个,依次传下去,100 次叫醒是串起来的。每一棒都要一次线程切换,所以全部醒来有一个微秒到毫秒级的“传递时间”,和线程数成正比。
← ④ 机制 4
7. 预测:JDK 21 上用虚拟线程跑 Spring MVC,日志框架用 logback,打日志会不会钉住载体线程?
不会因为 logback 钉住:它的 OutputStreamAppender 用的是 ReentrantLock(AQS),等锁时虚拟线程会被卸下。真正要查的是自己代码里、或老驱动里包着 IO 的 synchronized。
← ⑥ 坑 5 + 日常开发清单