jcstress 能稳定复现竞态条件,它通过自动调度线程交错、反复执行微操作并统计结果分布,精准暴露 AtomicInteger 跳变、volatile 可见性丢失、双重检查锁返回 null 等问题。

用 jcstress 复现竞态条件,比手写多线程测试靠谱得多
Java 并发问题最难的不是修复,是复现——手写 Thread + sleep + countDownLatch 很难稳定触发竞态,结果要么“没出错”,要么“报错了但看不出哪行代码漏锁”。jcstress 是 JDK 官方团队维护的并发压力测试工具,专为验证内存模型、原子性、可见性设计,它能自动调度线程交错、反复执行微小操作片段,并统计所有可能的结果分布。
常见错误现象:AtomicInteger.get() 返回值偶尔跳变、volatile 字段更新后另一线程仍读到旧值、双重检查锁单例返回 null。这些都不是偶发 bug,而是未同步导致的合法但非预期行为,jcstress 能把它们“打出来”。
- 必须用 JDK 8+(推荐 JDK 17),
jcstress依赖 JVM 的特定 intrinsics 和内存屏障语义 - 测试类不能有外部依赖(如 Spring、Log4j),需纯 POJO + JDK 原生并发原语
- 每个测试方法必须标注
@JCStressTest,并指定mode = Mode.Termination或Mode.Irrelevant,否则框架不识别 - 结果不是“通过/失败”,而是状态分布表:比如 99.9% 是
State.OK,0.1% 是State.LOST_UPDATE,这说明存在丢失更新
jcstress 测试里怎么写一个可被干扰的共享变量访问
核心原则:只暴露最小必要操作,让框架控制线程调度。不要加锁、不要 synchronized、不要 volatile(除非你正想测它)。典型结构是两个方法:一个写,一个读,共享一个字段。
使用场景:验证 int 字段是否线程安全、boolean 标志位能否被及时感知、对象引用赋值是否存在重排序。
立即学习“Java免费学习笔记(深入)”;
- 共享字段必须是
public volatile或public(非 volatile)——private字段会被 JVM 优化掉,jcstress看不见 - 写方法用
@Actor标注,读方法也用@Actor,不能用@Signal或@Arbiter混用 - 避免在测试方法里做 IO、异常抛出、循环计算——这些会干扰线程交错时机,掩盖真实内存问题
- 示例:两个线程分别执行
x = 1和y = x,期望y总是 1,但若x非 volatile,y可能为 0 —— 这就是jcstress能捕获的“合法乱序”
运行 jcstress 时最常踩的三个坑
不是代码写错,而是环境或配置没对,导致压测根本没跑起来,或者结果全是“UNKNOWN”。
- JVM 参数漏加
-XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI:这是jcstress注入字节码和观测内存屏障的前提,缺一不可 - Maven 打包用
mvn clean package后直接运行 jar,但没加-jar参数——正确命令是java -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -jar target/jcstress.jar - 测试类放在
src/test/java下却没在pom.xml中配置jcstress-maven-plugin的testSourceDirectory,导致编译时被跳过 - 误以为“没报错=线程安全”:
jcstress默认只跑几秒,低概率竞态可能漏掉;需显式加-t 60(60 秒)或-w 1000(1000 轮)提升触发率
结果怎么看:别只盯 ACCEPTED 和 FORBIDDEN
jcstress 报告里最该关注的是 OBSERVED 列的数值分布,而不是“有没有 FORBIDDEN”。很多看似“安全”的代码,在特定 CPU 架构(如 ARM)或 JVM 版本下会意外出现 OBSERVED 非零值。
性能影响:加 volatile 会让写操作慢 2–5 倍(取决于缓存一致性协议),但能消除 99% 的可见性问题;而 Unsafe.storeFence() 更轻量,但可读性差、JDK 版本兼容风险高。
-
State.LOST_UPDATE出现在计数器场景,说明 ++ 操作没原子性,得换AtomicInteger.incrementAndGet() -
State.REORDERED表示指令重排发生,比如构造函数里先赋值字段再设标志位,另一线程看到标志位 true 却读到未初始化字段 - 同一份代码在 x86 上
OBSERVED为 0,但在 AArch64 上非零?这不是 bug,是内存模型差异——得靠volatile或VarHandle显式同步
真正麻烦的不是发现竞态,而是确认“这个竞态在生产环境里到底会不会实际发生”。jcstress 给出的是理论可能性,而线上流量模式、GC 停顿、CPU 亲和性都会改变线程交错概率。所以压测结果只是起点,不是终点。










