先确认读的是哪一版
读 JUC 源码最容易踩的坑不是代码难,而是照着旧书找一个已经不存在的字段。JDK 25 的 AQS 在 JDK 14 就换过实现,旧书里的 waitStatus、addWaiter、SIGNAL、PROPAGATE 在本机源码里一处都没有。先花两分钟做一次名字核对,比读到一半发现对不上划算。
cd /tmp/juc-src/java.base/java/util/concurrent/locks
rg -o 'addWaiter|acquireQueued|unparkSuccessor|shouldParkAfterFailedAcquire|waitStatus' \
AbstractQueuedSynchronizer.java | wc -l
rg -o 'signalNext' AbstractQueuedSynchronizer.java | wc -l
rg -o 'nonfairTryAcquire' ReentrantLock.java | wc -l
0
7
0
第一行 0 不是 rg 没匹配到文件,而是这五个旧名字确实不存在;第二行说明新实现用 signalNext;第三行说明非公平只体现在 initialTryLock。ForkJoinPool 同理:scan、steal、pollAtMost 合计 0 次,pollScan 出现 5 次。以本机 src.zip 为准,旧书只当历史对照。
阅读顺序:从 AQS 到 FJP
不要从 ConcurrentHashMap 开始,也不要从 ForkJoinPool 开始。前者自成一派但跳过了 AQS 的排队语汇,后者代码密度最高,第一次读容易迷路。推荐顺序是 AQS → ReentrantLock → CHM → TPE → FJP:
AbstractQueuedSynchronizer(1.1–1.3)
├── ReentrantLock(2.1–2.3)
│ └── ConditionObject(2.2)
├── ReentrantReadWriteLock(3.1–3.2)
├── Semaphore / CountDownLatch(4.1–4.2)
└── ThreadPoolExecutor$Worker(11.1)
ConcurrentHashMap(5.1–5.3) ← 与 AQS 并列,共享 CAS/VarHandle 语汇
ThreadPoolExecutor(11.1–11.2) ← 先懂 Worker 的 AQS 锁
ForkJoinPool(12.1–12.2) ← 最后读,WorkQueue 是另一套队列
CompletableFuture(13.1–13.3) ← 依赖栈,建立在前面的执行器之上
| 顺序 | 类 | 读什么 | 读到什么程度算过关 |
|---|---|---|---|
| 1 | AbstractQueuedSynchronizer | Node 字段、state、acquire 大循环、cancelAcquire | 能画出「头节点是哨兵、真实等待者在后面」的队列图 |
| 2 | ReentrantLock | Sync.initialTryLock、NonfairSync 的 CAS 插队、FairSync 的 hasQueuedPredecessors | 能解释公平与非公平的差别只在哪两三个方法 |
| 3 | ConditionObject | await 的释放锁、ConditionNode.nextWaiter、enqueue + reacquire | 能说清条件队列如何回到主队列 |
| 4 | ConcurrentHashMap | table / sizeCtl / nextTable、putVal 的桶首 CAS、helpTransfer | 能说清读为什么不用锁、扩容谁在帮忙 |
| 5 | ThreadPoolExecutor | ctl 位域、addWorker 双重检查、runWorker 取任务 | 能说清线程数不涨时卡在哪个判断 |
| 6 | ForkJoinPool | WorkQueue 的 base / top / phase、runWorker、pollScan | 能说清 owner 与 thief 为什么从两端取 |
解压 src.zip:模块在前,包在后
JDK 9 之后 src.zip 的第一层是模块名,第二层才是包名。路径是 java.base/java/util/concurrent/...,不是 java/util/concurrent/...。这是解压时最常见的错误。
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
unzip -l "$JAVA_HOME/lib/src.zip" | head -4
Archive: /Users/bingrong.yan/.jdks/jdk-25.0.5+8/Contents/Home/lib/src.zip
Length Date Time Name
--------- ---------- ----- ----
3333 10-20-2026 00:00 java.base/apple/security/AppleProvider.java
rm -rf /tmp/juc-src && mkdir -p /tmp/juc-src
unzip -q "$JAVA_HOME/lib/src.zip" 'java.base/java/util/concurrent/*' -d /tmp/juc-src
find /tmp/juc-src -maxdepth 4 -type d | sort
/tmp/juc-src
/tmp/juc-src/java.base
/tmp/juc-src/java.base/java
/tmp/juc-src/java.base/java/util
/tmp/juc-src/java.base/java/util/concurrent
本机解压出 97 个 .java 文件。解压后先 grep 字段,确认自己记忆中的名字还在:
rg -n 'volatile Node prev|volatile Node next|Thread waiter|volatile int status' \
/tmp/juc-src/java.base/java/util/concurrent/locks/AbstractQueuedSynchronizer.java
rg -n 'static final int (WAITING|CANCELLED|COND) ' \
/tmp/juc-src/java.base/java/util/concurrent/locks/AbstractQueuedSynchronizer.java
469: volatile Node prev; // initially attached via casTail
470: volatile Node next; // visibly nonnull when signallable
471: Thread waiter; // visibly nonnull when enqueued
472: volatile int status; // written by owner, atomic bit ops by others
463: static final int WAITING = 1; // must be 1
464: static final int CANCELLED = 0x80000000; // must be negative
465: static final int COND = 2; // in a condition wait
waiter 不是 volatile,这不是 JDK 25 的笔误。节点先写 waiter,再通过 CAS 把节点挂到 tail;唤醒方先读 next / status 这些 volatile 字段,再读 waiter,可见性由前面的写读建立。看到旧书写 volatile Thread waiter,以本机源码为准。
javap 的四组组合
组合一:-p -constants 看节点与状态位
javap -p -constants 'java.util.concurrent.locks.AbstractQueuedSynchronizer$Node'
abstract class java.util.concurrent.locks.AbstractQueuedSynchronizer$Node {
volatile java.util.concurrent.locks.AbstractQueuedSynchronizer$Node prev;
volatile java.util.concurrent.locks.AbstractQueuedSynchronizer$Node next;
java.lang.Thread waiter;
volatile int status;
...
}
这一组看三件事:字段到底有几个、哪些是 volatile、有没有旧版的 waitStatus。status 是唯一的等待状态字段,prev / next 是双向链表,waiter 是等待线程。再配一条常量命令:
javap -p -constants java.util.concurrent.locks.AbstractQueuedSynchronizer \
| rg 'WAITING|CANCELLED|COND|private volatile int state'
static final int WAITING = 1;
static final int CANCELLED = -2147483648;
static final int COND = 2;
private volatile int state;
0x80000000 被打印成 -2147483648,因为 int 是有符号的。三个状态位与 1.1 的图能一一对上,说明你读的是新实现。
组合二:-p 看线程池的位域与新增字段
javap -p java.util.concurrent.ThreadPoolExecutor | head -24
public class java.util.concurrent.ThreadPoolExecutor extends java.util.concurrent.AbstractExecutorService {
private final java.util.concurrent.atomic.AtomicInteger ctl;
private static final int COUNT_BITS;
private static final int COUNT_MASK;
private static final int RUNNING;
private static final int SHUTDOWN;
private static final int STOP;
private static final int TIDYING;
private static final int TERMINATED;
private final java.util.concurrent.BlockingQueue<java.lang.Runnable> workQueue;
private final java.util.concurrent.locks.ReentrantLock mainLock;
private final java.util.HashSet<java.util.concurrent.ThreadPoolExecutor$Worker> workers;
private final java.util.concurrent.locks.Condition termination;
private final jdk.internal.vm.SharedThreadContainer container;
...
}
这一组看 ctl 的类型(仍是 AtomicInteger)、五个状态常量、workers 集合与 mainLock。jdk.internal.vm.SharedThreadContainer container 是 JDK 25 才出现的字段,旧书里没有;它和虚拟线程的线程容器有关,读 11 章时不要被它分散注意力。
组合三:-p -c 看原子类的真实调用
javap -p -c java.util.concurrent.atomic.AtomicInteger \
| sed -n '/getAndAdd(int)/,/ireturn/p'
public final int getAndAdd(int);
Code:
0: getstatic #13 // Field U:Ljdk/internal/misc/Unsafe;
3: aload_0
4: getstatic #17 // Field VALUE:J
7: iload_1
8: invokevirtual #38 // Method jdk/internal/misc/Unsafe.getAndAddInt:(Ljava/lang/Object;JI)I
11: ireturn
这一组专门用来反驳一个流行说法:「JDK 25 内部都换成 VarHandle 了」。字节码里清清楚楚是 jdk.internal.misc.Unsafe.getAndAddInt,VALUE 是字段偏移量。VarHandle 是给应用代码和 Striped64 这类新实现用的,不是所有原子类都迁移了。要确认某个类用没用 VarHandle,看它的常量池里有没有 java/lang/invoke/VarHandle,不要靠猜。
组合四:-p 看 ConcurrentHashMap 的扩容字段
javap -p java.util.concurrent.ConcurrentHashMap | rg \
'table;|nextTable|baseCount|sizeCtl|transferIndex|cellsBusy|counterCells|Unsafe U|SIZECTL'
volatile transient java.util.concurrent.ConcurrentHashMap$Node<K, V>[] table;
private volatile transient java.util.concurrent.ConcurrentHashMap$Node<K, V>[] nextTable;
private volatile transient long baseCount;
private volatile transient int sizeCtl;
private volatile transient int transferIndex;
private volatile transient int cellsBusy;
private volatile transient java.util.concurrent.ConcurrentHashMap$CounterCell[] counterCells;
这一组把 5.1 与 5.2 要用的字段一次看全:table 是当前桶数组,nextTable 是扩容中的新数组,sizeCtl 兼作初始化与扩容控制字,transferIndex 是迁移进度,counterCells 是分片计数。
在运行中的对象上看字段
javap 只给字段名,不给值。要看运行中的 state、head、tail,用 --add-opens 打开 java.base 的包,再反射读取。下面这个类先让一个线程阻塞在 lock() 上,再打印 AQS 队列:
import java.lang.reflect.Field;
import java.util.concurrent.locks.ReentrantLock;
public class InspectAQSQueue {
public static void main(String[] args) throws Exception {
ReentrantLock lock = new ReentrantLock();
lock.lock();
Thread t = new Thread(() -> lock.lock(), "blocked-borrower");
t.start();
Thread.sleep(500);
Field syncField = ReentrantLock.class.getDeclaredField("sync");
syncField.setAccessible(true);
Object sync = syncField.get(lock);
Class<?> aqs = sync.getClass();
while (!aqs.getName().endsWith("AbstractQueuedSynchronizer"))
aqs = aqs.getSuperclass();
Field stateF = aqs.getDeclaredField("state");
stateF.setAccessible(true);
System.out.println("state = " + stateF.get(sync));
Class<?> nodeClass = Class.forName(
"java.util.concurrent.locks.AbstractQueuedSynchronizer$Node");
Field headF = aqs.getDeclaredField("head");
headF.setAccessible(true);
Object node = headF.get(sync);
int i = 0;
while (node != null && i < 5) {
Field waiter = nodeClass.getDeclaredField("waiter");
Field status = nodeClass.getDeclaredField("status");
Field next = nodeClass.getDeclaredField("next");
waiter.setAccessible(true);
status.setAccessible(true);
next.setAccessible(true);
System.out.printf("node[%d] waiter=%s status=%d%n",
i, waiter.get(node), status.getInt(node));
node = next.get(node);
i++;
}
lock.unlock();
t.join();
}
}
不加参数直接跑,会在第一处 setAccessible 抛异常:
Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
Unable to make field private final java.util.concurrent.locks.ReentrantLock$Sync
java.util.concurrent.locks.ReentrantLock.sync accessible: module java.base
does not "opens java.util.concurrent.locks" to unnamed module
加上参数再跑:
java --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED \
-cp /tmp/juc-lab InspectAQSQueue
state = 1
node[0] waiter=null status=0
node[1] waiter=Thread[#25,blocked-borrower,5,main] status=1
这三行把 1.1 的图坐实了:head 是哨兵,waiter 为 null、status 为 0;真正的等待者在第二个节点,status = 1 就是 WAITING。IDE 里调试时同理:在 Run/Debug Configuration 的 VM options 加上同一串 --add-opens,在 ReentrantLock.lock 下断点,用 Evaluate Expression 执行上面的反射片段。没加参数,Evaluate 也会抛同一条 InaccessibleObjectException。要看 CHM 或 TPE 的内部字段,再加 --add-opens java.base/java.util.concurrent=ALL-UNNAMED。
线程转储:把栈帧对回源码
jcmd <pid> Thread.print
jstack <pid>
# 下面是 jcmd 的头部与 jstack 的线程段(摘录)
11763:
2026-10-29 01:31:42
Full thread dump OpenJDK 64-Bit Server VM (25.0.5+8-LTS mixed mode, sharing)
"loan-waiter-1" #26 [43267] prio=5 os_prio=31 cpu=0.32ms elapsed=4.39s
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@25.0.5/Native Method)
- parking to wait for <0x000000035fc19270> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(java.base@25.0.5/LockSupport.java:223)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(java.base@25.0.5/AbstractQueuedSynchronizer.java:790)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(java.base@25.0.5/AbstractQueuedSynchronizer.java:1030)
at java.util.concurrent.locks.ReentrantLock$Sync.lock(java.base@25.0.5/ReentrantLock.java:154)
at java.util.concurrent.locks.ReentrantLock.lock(java.base@25.0.5/ReentrantLock.java:323)
at DebugTarget.lambda$main$1(DebugTarget.java:26)
从下往上读:业务代码第 26 行调 lock(),ReentrantLock.lock 调 Sync.lock,Sync.lock 调外层 acquire,外层再调内层 acquire,最后停在 LockSupport.park。parking to wait for 后面的对象就是那把 NonfairSync,和反射看到的 head / tail 是同一个对象。
JFR:看谁在跑,而不是谁在等
jcmd <pid> JFR.start 可以在运行中的进程上开一段采样,不用重启:
jcmd <pid> JFR.start name=juc settings=profile duration=5s \
filename=/tmp/juc-lab/out/juc.jfr
sleep 7
jfr summary /tmp/juc-lab/out/juc.jfr | head -8
jfr print --events jdk.ExecutionSample --stack-depth 5 \
/tmp/juc-lab/out/juc.jfr | head -12
# 下面是 summary 与 print 的摘录
Version: 2.1
Chunks: 1
Start: 2026-10-28 17:31:43 (UTC)
Duration: 5 s
jdk.ExecutionSample 1599 15990
jdk.ExecutionSample {
sampledThread = "loan-counter-0" (javaThreadId = 28)
stackTrace = [
DebugTarget.lambda$main$2(long) line: 42
java.lang.Thread.run() line: 1474
]
}
第一次做这个实验时,靶子的线程全在 sleep 或 park,jfr summary 里 jdk.ExecutionSample 是 0 条。这不是录制失败:ExecutionSample 只采样处于 RUNNABLE 的 Java 线程,睡眠和阻塞的线程不会出现在执行样本里。加上四个持续做 LongAdder.increment() 的忙线程后,同样 5 秒采到 1599 条。看到 0 条,先问「有线程在跑吗」,而不是怀疑 jfr 坏了。
GC.class_histogram:确认分片真的发生了
jcmd <pid> GC.class_histogram | rg 'Striped64\$Cell|LongAdder|ReentrantLock'
51: 4 1120 java.util.concurrent.atomic.Striped64$Cell (java.base@25.0.5)
211: 1 32 [Ljava.util.concurrent.atomic.Striped64$Cell; (java.base@25.0.5)
224: 1 32 java.util.concurrent.atomic.LongAdder (java.base@25.0.5)
225: 1 32 java.util.concurrent.locks.ReentrantLock$NonfairSync (java.base@25.0.5)
四个 Cell 实例对应四个竞争线程,这就是 8.2 讲的「一个计数变成多个槽」。注意 GC.class_histogram 默认会触发一次 Full GC 再统计存活对象;只想看包含不可达对象的全量快照时加 -all,但那会更慢,线上慎用。
常见坑
- 把
javap -c当源码读。 字节码顺序不等于源码顺序,goto会把for(;;)拆成跳转;先用-p看结构,再回src.zip看方法体。 - 抄行号。 栈帧里的行号只对当前构建有效,跨小版本就会漂移;方法名和类名稳定得多。
- 反射不加
--add-opens。 JDK 9 之后java.base不向未命名模块开放,setAccessible必抛InaccessibleObjectException,这不是代码写错。
小结
读 JUC 源码的顺序由依赖决定:先 AQS 建立「state + 队列 + park」的语汇,再用 ReentrantLock 看最小完整子类,然后读 CHM 的桶与扩容,接着读 TPE 的 ctl 与 Worker,最后读 FJP 的 WorkQueue。工具链只有三件:src.zip 给源码,javap 给结构,jstack / jcmd / JFR 给运行态。每读一个字段,都在本机 rg 一次、javap 一次、跑一次靶子,比抄十页笔记可靠。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。