这一节解决什么问题
正文里的每一段代码,都需要一个能编译、能运行的环境。这一节把环境、目录、依赖、命名和实验工具一次交代清楚,后面不再重复。
如果你只想快速开始,按顺序做三件事:
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
java -version # 应输出 25.0.5
mvn -version # 应输出 3.9.12
然后建一个空目录,把下一节的 pom.xml 放进去,执行 mvn -q compile。能通过,就可以从第 1 章开始读。
版本基线
全书统一使用下面的版本,不跟随最新快照:
| 组件 | 版本 | 用途 |
|---|---|---|
| JDK | Temurin 25.0.5+8 | 编译与运行,--release 25 |
| Maven | 3.9.12 | 构建 |
| JUnit Jupiter | 5.14.4 | 单元测试 |
| JCStress | 0.16 | 竞态复现 |
| JMH | 1.37 | 微基准 |
| Lincheck | 2.44 | 线性一致性检查(仅卷三) |
本机口径:
Apple M1 Pro,10 核,32 GB
macOS 26.3 (25D125)
Temurin 25.0.5+8
Maven 3.9.12
java -version 的输出应该包含 25.0.5 和 LTS。如果你的机器默认 java 还是 8 或 17,先确认 JAVA_HOME 指向哪里:
echo $JAVA_HOME
/usr/libexec/java_home -V
macOS 上 /usr/libexec/java_home -V 会列出所有已安装的 JDK。本书不依赖 IDE,命令行能跑通就够了。
目录结构
libraryd-conc 是一个 Maven 多模块工程:
libraryd-conc/
├── pom.xml
├── conc-core/ src/main/java/com/plume/conc/core/ 领域模型与 LoanService 实现
├── conc-lab/ src/main/java/com/plume/conc/lab/ JCStress、JMH、事故复现
└── conc-ops/ src/main/java/com/plume/conc/ops/ 指标、诊断、压测脚本
三个模块的职责:
conc-core:领域模型与LoanService的各种实现。卷一和卷二的主战场。conc-lab:JCStress 测试、JMH 基准、事故复现。它依赖conc-core,不反过来。conc-ops:指标、线程诊断、压测脚本。卷三才用,卷一可以暂时留空。
父 pom.xml 的关键部分是编译级别和模块列表:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.plume.conc</groupId>
<artifactId>libraryd-conc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>conc-core</module>
<module>conc-lab</module>
<module>conc-ops</module>
</modules>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.14.4</junit.version>
<jcstress.version>0.16</jcstress.version>
<jmh.version>1.37</jmh.version>
</properties>
</project>
conc-core 只依赖 JUnit,测试范围:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
不引入 Spring、Netty、Guava、Disruptor。原因是这一卷要看清 JDK 自己提供了什么。第三方并发库留到需要它们的章节再讨论,不作为示例的前提。
领域模型
conc-core 的类型在序里已经出现过,这里给完整定义。后续章节直接使用,不再重复。
package com.plume.conc.core;
import java.time.Instant;
import java.util.Optional;
public record Book(String isbn, String title, int stock) {}
public record Member(String memberId, String name, int quota) {}
public record Loan(String loanId, String isbn, String memberId, Instant dueAt) {}
public record Reservation(String loanId, String isbn, String memberId, Instant dueAt) {}
public enum ErrorCode {
BOOK_NOT_FOUND, OUT_OF_STOCK, QUOTA_EXCEEDED, CONFLICT, INTERRUPTED
}
public final class LoanException extends RuntimeException {
private final ErrorCode code;
public LoanException(ErrorCode code, String message) {
super(message);
this.code = code;
}
public ErrorCode code() {
return code;
}
}
public interface LoanService {
Reservation reserve(String isbn, String memberId);
boolean returnBook(String loanId);
}
public interface BookRepository {
Optional<Book> findByIsbn(String isbn);
void save(Book book);
}
public interface LoanRepository {
Optional<Loan> findById(String loanId);
void save(Loan loan);
}
Book 用 record 是有意的:不可变对象天然线程安全,不需要同步就能安全发布。后面讲「线程安全的四类策略」时,这是第一类。
内存仓库在实验里用得最多,给一个最小实现:
package com.plume.conc.core;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public final class InMemoryBookRepository implements BookRepository {
private final ConcurrentMap<String, Book> books = new ConcurrentHashMap<>();
public InMemoryBookRepository() {
books.put("978-7-111-00001", new Book("978-7-111-00001", "并发编程实践", 100));
books.put("978-7-111-00002", new Book("978-7-111-00002", "JVM 内核", 10));
}
@Override
public Optional<Book> findByIsbn(String isbn) {
return Optional.ofNullable(books.get(isbn));
}
@Override
public void save(Book book) {
books.put(book.isbn(), book);
}
}
注意 ConcurrentHashMap 只保证单次 get/put 的线程安全,不保证「先 get 再 put」这一组操作的原子性。第 2 章会专门讲这个陷阱。
实验工具
JCStress
conc-lab 的 pom.xml 需要同时引入 JCStress、JMH 和它们的注解处理器:
<dependencies>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>${jcstress.version}</version>
</path>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
这段配置不是可选的。JDK 23 之后,javac 默认不再隐式运行 classpath 上的注解处理器;如果不显式列出 annotationProcessorPaths,JCStress 不会生成 META-INF/TestList,运行时会在 TestList.getTests() 里抛 NullPointerException。这个坑在本机实测过,报错长这样:
Exception in thread "main" java.lang.NullPointerException
at java.base/java.io.Reader.<init>(Reader.java:286)
at org.openjdk.jcstress.infra.runners.TestList.getTests(TestList.java:51)
把两个处理器都加进去之后,正常。注意 JCStress 和 JMH 的处理器要一起写,只写一个会把另一个的生成物弄丢。
打成可执行 jar 用 maven-shade-plugin:3.6.0,ManifestResourceTransformer 的 mainClass 设为 org.openjdk.jcstress.Main。这一段配置较长但不涉及并发知识,按插件文档写即可。
运行单个测试:
mvn -q package -DskipTests
java -jar conc-lab/target/conc-lab-1.0-SNAPSHOT.jar -t LostUpdate -m quick
JCStress 启动时会探测 JVM 能力,其中一行是:
----- [N/A] Checking for biased locking support
Unrecognized VM option 'UseBiasedLocking'
这是 JCStress 在尝试启用偏向锁。JDK 25 已经移除这个选项,所以探测失败并标记 N/A,不影响测试。看到这行不用管。
JMH
JMH 的基准类在 conc-lab 里,运行方式和 JCStress 共用一个 shade jar:
java -cp conc-lab/target/conc-lab-1.0-SNAPSHOT.jar org.openjdk.jmh.Main LongAdderBench
JMH 的输出会给出 Score 和 Error。书里引用时会原样保留误差,不为了好看删掉。
JFR 与诊断命令
JDK 自带 JFR,不需要额外安装:
java -XX:StartFlightRecording=filename=app.jfr,duration=30s -cp target/classes com.plume.conc.core.Main
jfr print --events jdk.VirtualThreadPinned app.jfr
jfr summary app.jfr
线程转储用 jcmd:
jcmd <pid> Thread.print
jcmd <pid> Thread.print -l # 同时打印锁信息
macOS 上没有 ss,看端口用 lsof -i :8080;本卷基本不涉及端口,命令集中在附录 B。
preview 的用法
这两个 API 的 preview 状态在 JDK 25 并不相同,必须分开记:
| API | 状态 | 编译/运行参数 |
|---|---|---|
ScopedValue | 已转正 | 无需任何 preview 参数 |
StructuredTaskScope | preview | 编译、运行都要加 --enable-preview |
用 StructuredTaskScope 必须同时满足三件事:
- 编译时加
--enable-preview; - 运行时加
--enable-preview; - 编译和运行的 JDK 版本一致(都是 25)。
Maven 里这样开:
mvn -q compile -Dmaven.compiler.enablePreview=true
java --enable-preview -cp target/classes com.plume.conc.core.StructuredDemo
如果只加运行时参数、不加编译参数,javac 会拒绝使用 preview API;如果两个都加了但版本不一致,JVM 会报 Preview features are not enabled 或类文件版本不匹配。
preview 的含义是:API 可能在下个版本变化,不保证二进制兼容。第 15 章会给出 StructuredTaskScope 的替代方案,生产代码是否采用由你判断。本卷不能因为「preview 可能变」就跳过不讲,结构化并发是 JDK 25 并发能力的核心;也不能假装它已经稳定,所以每处使用都会标注。第 7 章的 ScopedValue 不受这条影响:它已经转正,可以按稳定 API 写。
JDK 25 的已移除项
写书过程中,最容易过时的是「书上说可以,实际已经不能用了」。下面这些在本机实测确认:
| API / 选项 | 状态 | 替代 |
|---|---|---|
-XX:+UseBiasedLocking | 已移除,启动报错 | 无,锁升级不再有偏向阶段 |
Thread.stop() | 已弃用,调用抛 UnsupportedOperationException | 中断 + 协作式取消 |
Thread.suspend() / resume() | 已移除 | LockSupport.park / unpark |
Thread.getId() | 已弃用 | threadId() |
SecurityManager | 永久禁用 | 无,改用进程隔离 |
sun.misc.Unsafe 内存方法 | 已弃用 | VarHandle、FFM |
第 4 章会解释偏向锁为什么被移除,第 6 章讲为什么 stop 不能再用,第 9 章讲 VarHandle 如何替代 Unsafe 的原子操作。这里先列出来,是为了避免你照着旧代码调试半天。
怎么验证书里的结论
每一节的验证方式不完全一样,但大体分四步:
- 编译:代码块里的类能通过
mvn -q compile。个别示例为了篇幅会省略 import,编译时按提示补。 - 跑实验:涉及竞态的章节给 JCStress 测试,涉及性能的章节给 JMH 基准,涉及运行时行为的章节给 JFR 命令。
- 看输出:书里贴的输出都来自本机实跑。你的数字会不同,但相对关系应该一致;如果不一致,先检查 CPU 核数和 JVM 参数。
- 改一行再跑:把
volatile删掉、把while改成if、把有界队列换成无界队列,看看结果怎么变。并发知识很难靠读会,改一行跑一次是最有效的办法。
有一个边界要说清楚:书里的代码是教学代码,不是生产库。比如 InMemoryBookRepository 只用于实验,真实系统要接数据库;UnsafeLoanService 是故意写错的,不能复制到项目里。每个实现都会标注用途。
接下来
环境就绪之后,从第 1 章开始。第 1 章先回答一个看起来很简单的问题:Java 的线程到底是什么,创建一个线程要付多少代价。这个问题不清楚,后面讨论线程池大小、虚拟线程为什么快,都会失去参照。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。