《Java 并发编程·JUC 源码与容器》前言:源码版本、工具与约定

《Java 并发编程·JUC 源码与容器》前言:交代 JDK 25 源码的获取方式与包结构、用 unzip 与 javap 定位事实的方法、JMH 与 JCStress 的实验环境、源码摘录的标注约定,并列出本机核实过的 JDK 25 与旧版实现差异清单。

这一篇解决什么问题

读源码这件事,卡住人的往往不是看不懂代码,而是三件更基础的事:源码在哪、版本对不对、怎么确认自己看到的不是记忆里的旧版本。

这一篇把这三件事定下来。读完你应该能:

  • 在自己的机器上解出 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/11Lock 体系、AQS、Condition、StampedLock
java/util/concurrent/atomic/18原子类、Striped64、字段更新器

根包最大,是因为历史原因:Executor 系列、BlockingQueue 系列、并发容器都直接放在 java.util.concurrent 下,没有按功能再分子包。读的时候按这个顺序建立地图比较省力:

  1. 同步器(CountDownLatch、CyclicBarrier、Semaphore、Phaser、Exchanger):都建在 AQS 上,读完第 1 章再看,结构会非常清楚。
  2. 锁(locks/ 下 11 个文件):核心是 AbstractQueuedSynchronizer 与 long 版的 AbstractQueuedLongSynchronizer,外面套着 ReentrantLock、ReentrantReadWriteLock、StampedLock。
  3. 容器与队列:ConcurrentHashMap 一个类顶半本书,ConcurrentSkipListMap 是另一套思路,CopyOnWriteArrayList 最简单;队列把 ArrayBlockingQueue 和 LinkedBlockingQueue 对比着看,锁粒度差异最直观。
  4. 原子类:先看 AtomicInteger,再看 Striped64,最后看 LongAdder。
  5. 执行器: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 行

这组数字说明阅读策略应该不同:

类规模(行)读法
ReentrantLock799可以整篇读,一半是 Javadoc
AbstractQueuedSynchronizer1990只读 acquire、release、signalNext、cancelAcquire 四处,其余跳过
ConcurrentHashMap6402按操作读:get → putVal → transfer → sumCount
ThreadPoolExecutor2096按生命周期读:execute → addWorker → runWorker → processWorkerExit
ForkJoinPool4422只读 WorkQueue、scan、steal 相关部分
CompletableFuture3042只读 Completion 继承体系和一个 thenCompose 的链路
Striped64383全读,它是「小文件大设计」的典型

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 节点状态字段waitStatusstatus
AQS 状态常量SIGNAL、CANCELLED、CONDITION、PROPAGATEWAITING = 1、CANCELLED = 0x80000000、COND = 2
AQS 入队addWaiteracquire(...) 主循环内 casTail
AQS 唤醒unparkSuccessorsignalNext(head) / signalNextIfShared(head)
AQS 出队setHead + acquireQueued主循环内改 head
共享传播setHeadAndPropagate + PROPAGATEsignalNextIfShared 判断 SharedNode
条件队列转移transferForSignalenqueue(ConditionNode) + reacquire(...)
ReentrantLock 非公平入口lock() 里两次 CASNonfairSync.initialTryLock()
ReentrantLock 的 nonfairTryAcquire存在不存在(只剩 Semaphore 的 nonfairTryAcquireShared)
读写锁基类AbstractQueuedSynchronizer,高低 16 位AbstractQueuedLongSynchronizer,高低 32 位
读写锁写锁重入上限65535Integer.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 试读一节,再回来决定走哪条路径。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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