这一篇解决什么问题
读源码这件事,卡住人的往往不是看不懂代码,而是三件更基础的事:源码在哪、版本对不对、怎么确认自己看到的不是记忆里的旧版本。
这一篇把这三件事定下来。读完你应该能:
- 在自己的机器上解出 JUC 的源码,并且知道每个文件属于哪个子包;
- 用
javap确认一个字段或常量在编译产物里真实存在、真实取值; - 用 JMH 和 JCStress 复现本卷的实验;
- 判断自己机器上的实现是「新 AQS」还是「旧 AQS」。
最后一条比听起来重要。AQS 的内部实现被整体重写过,新旧两套的字段名、方法名完全不同。你需要先知道自己站在哪个版本上,再判断书里的描述对不对应。
版本基线
本卷所有代码、命令、输出的环境:
Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)
Temurin 25.0.5+8(build 25.0.5+8-LTS)
Maven 3.9.12
JMH 1.37
JCStress 0.16
JUnit Jupiter 5.14.4
java -version 的本机输出:
openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)
安装路径按个人习惯不同,本文统一用 $JAVA_HOME:
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
后面的命令都假设这两个变量已经设好。
JUC 的公开 API 极其稳定,ReentrantLock 从 JDK 5 到 25 没增删过公开方法,但内部实现改过好几轮,而本卷讲的正是内部实现。钉死版本是为了让「本机输出」有意义:同一条命令在不同 JDK 上输出不同,不说清版本,读者就分不清差异来自代码还是环境。
新旧实现的分界
AQS 有两套实现,靠 addWaiter 这个方法名区分。用 OpenJDK 官方 tag 的源码可以确认分界:
python3 - <<'PY'
import urllib.request
BASE = "https://raw.githubusercontent.com/openjdk/jdk/"
PATH = "/src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java"
for tag in ["jdk-13+33", "jdk-14+36", "jdk-17+35", "jdk-21+35"]:
t = urllib.request.urlopen(BASE + tag + PATH, timeout=25).read().decode()
print(tag, "addWaiter:", t.count("addWaiter"), "waitStatus:", t.count("waitStatus"))
PY
本机输出:
jdk-13+33 addWaiter: 8 waitStatus: 42
jdk-14+36 addWaiter: 0 waitStatus: 0
jdk-17+35 addWaiter: 0 waitStatus: 0
jdk-21+35 addWaiter: 0 waitStatus: 0
所以分界是 JDK 13 → 14:JDK 13 及以前是旧实现,JDK 14 及以后是新实现。这意味着 JDK 17、21、25 用的都是新实现,只是细节随版本演进——本机上 Temurin 25 的 AQS 是 1990 行,jdk-21+35 是 1945 行,jdk-17+35 是 1856 行。
这份数据来自 OpenJDK 的 GitHub tag 源码,不是本机安装的 JDK(本机只装了 Zulu 8u482 与 Temurin 25.0.5+8)。你可以在自己的机器上用上面那段脚本复核。
源码从哪来
JDK 的安装目录里自带 lib/src.zip,解压就是完整的 JDK 源码。
ls -l "$JAVA_HOME/lib/src.zip"
本机输出:
-rw-r--r-- 1 user staff 53700469 10-20 00:00 /Users/user/.jdks/jdk-25.0.5+8/Contents/Home/lib/src.zip
约 51 MB,覆盖全部 JDK 模块。
目录结构:模块在前,包在后
src.zip 的顶层不是 java/,而是模块名。JUC 全部在 java.base 模块下:
java.base/java/util/concurrent/...
java.base/java/util/concurrent/locks/...
java.base/java/util/concurrent/atomic/...
java.base/java/lang/invoke/...
IDE 展示的是「包 → 类」,src.zip 展示的是「模块 → 包 → 类」。用 unzip -l 找文件却找不到,多半是漏了 java.base/ 前缀。
只解 JUC 相关的部分:
mkdir -p /tmp/juc-src && cd /tmp/juc-src
unzip -oq "$JAVA_HOME/lib/src.zip" \
'java.base/java/util/concurrent/*.java' \
'java.base/java/util/concurrent/locks/*.java' \
'java.base/java/util/concurrent/atomic/*.java' \
'java.base/java/lang/invoke/*.java'
解出来的文件路径就是包路径。比如 AbstractQueuedSynchronizer 在:
java.base/java/util/concurrent/locks/AbstractQueuedSynchronizer.java
JUC 的包结构
本机统计 java.util.concurrent 下的 .java 文件一共 97 个(unzip -Z1 "$JAVA_HOME/lib/src.zip" | grep -c '^java\.base/java/util/concurrent/.*\.java$')。按子包分布:
| 位置 | 文件数 | 内容 |
|---|---|---|
java/util/concurrent/ 根 | 68 | 执行器、队列、容器、CompletableFuture、同步器 |
java/util/concurrent/locks/ | 11 | Lock 体系、AQS、Condition、StampedLock |
java/util/concurrent/atomic/ | 18 | 原子类、Striped64、字段更新器 |
根包最大,是因为历史原因:Executor 系列、BlockingQueue 系列、并发容器都直接放在 java.util.concurrent 下,没有按功能再分子包。读的时候按这个顺序建立地图比较省力:
- 同步器(
CountDownLatch、CyclicBarrier、Semaphore、Phaser、Exchanger):都建在 AQS 上,读完第 1 章再看,结构会非常清楚。 - 锁(
locks/下 11 个文件):核心是AbstractQueuedSynchronizer与long版的AbstractQueuedLongSynchronizer,外面套着ReentrantLock、ReentrantReadWriteLock、StampedLock。 - 容器与队列:
ConcurrentHashMap一个类顶半本书,ConcurrentSkipListMap是另一套思路,CopyOnWriteArrayList最简单;队列把ArrayBlockingQueue和LinkedBlockingQueue对比着看,锁粒度差异最直观。 - 原子类:先看
AtomicInteger,再看Striped64,最后看LongAdder。 - 执行器:
ThreadPoolExecutor、ForkJoinPool、CompletableFuture,三章各一个。
包内私有类也值得看
src.zip 里能看到编译后不可见的包级私有类。JDK 25 的 java.util.concurrent 下就有一个新面孔:
final class DelayScheduler extends Thread {
它是 ForkJoinPool 的延迟/周期任务调度器,用 4 叉堆维护触发时间,把到期的 ScheduledForkJoinTask 转交给池或直接执行。这个类在 Javadoc 里查不到,javap 也只能看到包级成员,但它决定了 ForkJoinPool.schedule 的行为。第 12 章会提到它。
说这些是为了养成一个习惯:看到 final class Xxx 没有 public,不要跳过。JUC 里很多关键逻辑在包级私有类里,公开类只是壳。
关键类的规模
读之前先知道哪些是「大部头」。用 unzip -p "$JAVA_HOME/lib/src.zip" <path> | wc -l 逐个统计,本机结果:
locks/AbstractQueuedSynchronizer 1990 行
ConcurrentHashMap 6402 行
ThreadPoolExecutor 2096 行
ForkJoinPool 4422 行
CompletableFuture 3042 行
locks/ReentrantLock 799 行
atomic/Striped64 383 行
这组数字说明阅读策略应该不同:
| 类 | 规模(行) | 读法 |
|---|---|---|
ReentrantLock | 799 | 可以整篇读,一半是 Javadoc |
AbstractQueuedSynchronizer | 1990 | 只读 acquire、release、signalNext、cancelAcquire 四处,其余跳过 |
ConcurrentHashMap | 6402 | 按操作读:get → putVal → transfer → sumCount |
ThreadPoolExecutor | 2096 | 按生命周期读:execute → addWorker → runWorker → processWorkerExit |
ForkJoinPool | 4422 | 只读 WorkQueue、scan、steal 相关部分 |
CompletableFuture | 3042 | 只读 Completion 继承体系和一个 thenCompose 的链路 |
Striped64 | 383 | 全读,它是「小文件大设计」的典型 |
ConcurrentHashMap 6402 行里,光 Javadoc 和方法注释就占了大半。本卷不会逐行翻译,只挑设计决策讲。
三件工具
一、unzip + grep:确认一个名字存不存在
最常见的问题:这个字段/方法在当前版本里还有吗。一条命令就能回答:
S="$JAVA_HOME/lib/src.zip"
for m in addWaiter initialTryLock; do
echo "$m: $(unzip -p "$S" 'java.base/java/util/concurrent/locks/*.java' | grep -c "$m")"
done
addWaiter: 0
initialTryLock: 8
注意 grep -c 统计的是行数不是次数,一个方法名在一行里出现两次只算一次。够用了,我们要的是「存不存在」。
unzip -p 支持一个 glob,不支持多个。要跨多个目录统计,先解压再用 grep -r:
cd /tmp/juc-src
grep -rn 'nonfairTryAcquire' java.base/java/util/concurrent/
本机输出只有四行,全部来自 Semaphore.java:
java.base/java/util/concurrent/Semaphore.java:183: final int nonfairTryAcquireShared(int acquires) {
java.base/java/util/concurrent/Semaphore.java:235: return nonfairTryAcquireShared(acquires);
java.base/java/util/concurrent/Semaphore.java:369: return sync.nonfairTryAcquireShared(1) >= 0;
java.base/java/util/concurrent/Semaphore.java:536: return sync.nonfairTryAcquireShared(permits) >= 0;
ReentrantLock.java 里一处都没有。很多教材讲非公平锁时引用的 nonfairTryAcquire,在 JDK 25 已经不在 ReentrantLock 里了。
二、javap:确认编译产物里有什么
javap -p 列出所有成员(包括 private),-c 反汇编字节码,-constants 显示 static final 常量的值。
javap -p '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;
private static final long STATUS;
private static final long NEXT;
private static final long PREV;
java.util.concurrent.locks.AbstractQueuedSynchronizer$Node();
final boolean casPrev(java.util.concurrent.locks.AbstractQueuedSynchronizer$Node, java.util.concurrent.locks.AbstractQueuedSynchronizer$Node);
final boolean casNext(java.util.concurrent.locks.AbstractQueuedSynchronizer$Node, java.util.concurrent.locks.AbstractQueuedSynchronizer$Node);
final int getAndUnsetStatus(int);
final void setPrevRelaxed(java.util.concurrent.locks.AbstractQueuedSynchronizer$Node);
final void setStatusRelaxed(int);
final void clearStatus();
static {};
}
这份输出里没有 waitStatus,只有 status。照着记忆写 waitStatus,解释就和真实实现对不上了。
看常量值用 -constants:
javap -p -constants java.util.concurrent.ConcurrentHashMap | grep TREEIFY
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
三、IDE:跳转与调用链
unzip 和 javap 负责「确认」,IDE 负责「浏览」。把 src.zip 挂到 IDE 上,Cmd+点击 跳声明、Find Usages 看调用链。本卷给的阅读顺序都按调用链排,比如第 5 章的 put → putVal → (casTabAt | synchronized 桶首 | treeifyBin | helpTransfer | addCount),在 IDE 里一路跟下去比读文章快。文章的价值在于告诉你「跟到哪该停」以及「为什么这条路径长这样」。
实验工具
本卷的实验分两类:正确性用 JCStress,性能用 JMH。conc-lab 的 pom.xml 关键部分(卷一已用同一份跑过实验):
<maven.compiler.release>25</maven.compiler.release>
<!-- dependencies -->
<dependency><groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId><version>0.16</version></dependency>
<dependency><groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId><version>1.37</version></dependency>
<dependency><groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId><version>1.37</version>
<scope>provided</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId><version>5.14.4</version>
<scope>test</scope></dependency>
注解处理器必须同时配 JCStress 和 JMH 两个,否则 @JCStressTest 和 @Benchmark 都不会生成代码。maven-compiler-plugin 的 annotationProcessorPaths 配法见卷一的实验工程,本卷沿用。
JMH 的读法
JMH 的实验写得再规范,结论也只对这台机器成立。本卷引用 JMH 数字时遵守两条:只引用结构性结论(「公平锁比非公平锁慢一个数量级」,而不是「12.3 万 vs 210 万 ops/s」,后者换个机器就变);标注负载(本机是笔记本,负载偏高时会写出来,或改用「CAS 失败次数」这类不依赖墙钟时间的指标)。
JCStress 的读法
JCStress 输出的是「结果矩阵」:每种可能的执行结果出现了多少次。判定标准不是「只有一种结果」,而是「不允许出现的结果一次都没有出现」。卷一用它验证 volatile 与 CAS 的语义,本卷用它验证 AQS 的唤醒与 Condition 的转移,配置方式不变。
源码摘录的约定
四条规矩,前面一篇已经说过,这里再明确一次,方便你回头查:
| 约定 | 含义 |
|---|---|
首行标 // JDK 25 源码摘录,已删减 | 来自 Temurin 25.0.5+8 的 src.zip,删过无关行 |
| 不写行号 | 只写类名 + 方法名,跨版本稳定 |
| 解释必须落在摘录里 | 正文提到的字段/方法,摘录中必须出现 |
私有字段先 javap -p 核实 | 不凭记忆写字段名 |
删减的通常是参数校验、日志、@ReservedStackAccess 之类的注解、只处理 OOM 的分支和大段注释。
JDK 25 相对旧版的差异清单
这张表是本机逐条核实过的,写作和阅读都用得上。左列是旧版教材里常见的写法,右列是 JDK 25 的实际情况。
| 项 | 旧版(JDK 8 时代) | JDK 25 实际 |
|---|---|---|
| AQS 节点状态字段 | waitStatus | status |
| AQS 状态常量 | SIGNAL、CANCELLED、CONDITION、PROPAGATE | WAITING = 1、CANCELLED = 0x80000000、COND = 2 |
| AQS 入队 | addWaiter | acquire(...) 主循环内 casTail |
| AQS 唤醒 | unparkSuccessor | signalNext(head) / signalNextIfShared(head) |
| AQS 出队 | setHead + acquireQueued | 主循环内改 head |
| 共享传播 | setHeadAndPropagate + PROPAGATE | signalNextIfShared 判断 SharedNode |
| 条件队列转移 | transferForSignal | enqueue(ConditionNode) + reacquire(...) |
ReentrantLock 非公平入口 | lock() 里两次 CAS | NonfairSync.initialTryLock() |
ReentrantLock 的 nonfairTryAcquire | 存在 | 不存在(只剩 Semaphore 的 nonfairTryAcquireShared) |
| 读写锁基类 | AbstractQueuedSynchronizer,高低 16 位 | AbstractQueuedLongSynchronizer,高低 32 位 |
| 读写锁写锁重入上限 | 65535 | Integer.MAX_VALUE |
这张表不是让你背,是让你在遇到「书里说的和我看到的不一样」时有个对照。第 1 到第 3 章会逐条展开。
怎么验证书里的结论
三条命令覆盖大部分场景。
# 1. 字段或常量是否存在
javap -p java.util.concurrent.locks.AbstractQueuedSynchronizer \
| grep -E 'head|tail|state'
# 2. 方法名是否存在
unzip -p "$JAVA_HOME/lib/src.zip" \
'java.base/java/util/concurrent/locks/*.java' | grep -c 'addWaiter'
// 3. 运行期行为是否与预期一致
public class Probe {
public static void main(String[] args) {
var lock = new java.util.concurrent.locks.ReentrantLock(true);
lock.lock();
try {
System.out.println("holdCount=" + lock.getHoldCount());
} finally {
lock.unlock();
}
}
}
运行期能观察到的行为(getHoldCount、getQueueLength、hasQueuedThreads)比源码更适合作为「结论」,因为它们不随内部重写变化。
接下来
配置好环境、确认版本之后,按顺序读下去即可。时间有限的话,第 1 章(AQS)和第 5 章(ConcurrentHashMap)投入产出比最高:前者是后面四章的地基,后者是排障中出现频率最高的容器。目录与学习路径一篇里给了三条读法,包括按症状跳读的对照表;也可以先跳到 1.1 试读一节,再回来决定走哪条路径。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。