《Java 并发编程·JUC 源码与容器》附录 B 源码阅读路径与调试技巧

从 JDK 25 的 src.zip 解压开始,给出 AQS、ReentrantLock、ConcurrentHashMap、ThreadPoolExecutor、ForkJoinPool 的依赖阅读顺序;用四组本机 javap 输出说明该看什么,再用 --add-opens 反射、jstack、jcmd 线程转储与 JFR 对齐源码字段与运行态。

先确认读的是哪一版

读 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)     ← 依赖栈,建立在前面的执行器之上
顺序类读什么读到什么程度算过关
1AbstractQueuedSynchronizerNode 字段、state、acquire 大循环、cancelAcquire能画出「头节点是哨兵、真实等待者在后面」的队列图
2ReentrantLockSync.initialTryLock、NonfairSync 的 CAS 插队、FairSync 的 hasQueuedPredecessors能解释公平与非公平的差别只在哪两三个方法
3ConditionObjectawait 的释放锁、ConditionNode.nextWaiter、enqueue + reacquire能说清条件队列如何回到主队列
4ConcurrentHashMaptable / sizeCtl / nextTable、putVal 的桶首 CAS、helpTransfer能说清读为什么不用锁、扩容谁在帮忙
5ThreadPoolExecutorctl 位域、addWorker 双重检查、runWorker 取任务能说清线程数不涨时卡在哪个判断
6ForkJoinPoolWorkQueue 的 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 一次、跑一次靶子,比抄十页笔记可靠。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收