《Java 并发编程·线程与内存模型》前言:环境、项目与读法

交代本书的运行环境与版本基线:JDK 25、Maven 3.9.12、JCStress 0.16、JMH 1.37,给出 libraryd-conc 三模块的目录与 pom 配置、领域模型完整定义、preview 开关,以及 JCStress 在 JDK 25 上必须显式配置注解处理器的实际坑。

这一节解决什么问题

正文里的每一段代码,都需要一个能编译、能运行的环境。这一节把环境、目录、依赖、命名和实验工具一次交代清楚,后面不再重复。

如果你只想快速开始,按顺序做三件事:

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 章开始读。

版本基线

全书统一使用下面的版本,不跟随最新快照:

组件版本用途
JDKTemurin 25.0.5+8编译与运行,--release 25
Maven3.9.12构建
JUnit Jupiter5.14.4单元测试
JCStress0.16竞态复现
JMH1.37微基准
Lincheck2.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 参数
StructuredTaskScopepreview编译、运行都要加 --enable-preview

用 StructuredTaskScope 必须同时满足三件事:

  1. 编译时加 --enable-preview;
  2. 运行时加 --enable-preview;
  3. 编译和运行的 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 的原子操作。这里先列出来,是为了避免你照着旧代码调试半天。

怎么验证书里的结论

每一节的验证方式不完全一样,但大体分四步:

  1. 编译:代码块里的类能通过 mvn -q compile。个别示例为了篇幅会省略 import,编译时按提示补。
  2. 跑实验:涉及竞态的章节给 JCStress 测试,涉及性能的章节给 JMH 基准,涉及运行时行为的章节给 JFR 命令。
  3. 看输出:书里贴的输出都来自本机实跑。你的数字会不同,但相对关系应该一致;如果不一致,先检查 CPU 核数和 JVM 参数。
  4. 改一行再跑:把 volatile 删掉、把 while 改成 if、把有界队列换成无界队列,看看结果怎么变。并发知识很难靠读会,改一行跑一次是最有效的办法。

有一个边界要说清楚:书里的代码是教学代码,不是生产库。比如 InMemoryBookRepository 只用于实验,真实系统要接数据库;UnsafeLoanService 是故意写错的,不能复制到项目里。每个实现都会标注用途。

接下来

环境就绪之后,从第 1 章开始。第 1 章先回答一个看起来很简单的问题:Java 的线程到底是什么,创建一个线程要付多少代价。这个问题不清楚,后面讨论线程池大小、虚拟线程为什么快,都会失去参照。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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