概述
在 Gravitino 中,我们使用 JCStress 框架进行并发压力测试,以识别线程安全相关的微妙缺陷。这类问题往往难以通过传统的单元测试捕捉,例如:
- 数据竞争(Race Condition)
- 内存可见性问题(Visibility Violation)
- 原子性缺失(Atomicity Violation)
JCStress 能有效帮助我们验证并发代码的正确性,确保缓存、元数据等核心组件具备良好的线程安全保障。
什么是 JCStress?
JCStress 是 Oracle 官方提供的 Java 并发测试工具,专为检测并发场景下的非确定性错误而设计。其主要特性包括:
- 在多个线程间并发执行操作,模拟实际竞争环境;
- 使用大量线程交错(Interleaving)组合反复运行;
- 揭示极低概率但高危的并发缺陷;
- 将测试结果分为三类:可接受(ACCEPTABLE)、禁止(FORBIDDEN)、值得关注(INTERESTING)。
如何运行测试
在 Gravitino 中使用 jcstress-gradle-plugin
将 JCStress 集成到 Gradle build 系统中。
该插件的优势如下:
- 自动生成测试运行器(test harness);
- 提供内置 Gradle 任务(如
:core:jcstress
); - 自动生成报告,在
build/reports/jcstress/
下生成详细的 HTML 报告。
运行测试的命令
运行所有 JCStress 测试:
./gradlew :core:jcstress
仅运行指定测试类(正则匹配):
gradle jcstress --tests "MyFirstTest|MySecondTest"
执行完毕后,可以在如下位置查看报告: core/build/reports/jcstress/index.html
测试运行时间取决于测试复杂度与系统 CPU 资源,通常为数分钟至数十分钟不等。
解读测试结果
JCStress 的测试结果以表格形式展示,包括:
- 每种观察结果的内容与出现频率;
- 每个结果的期望分类(ACCEPTABLE/FORBIDDEN/INTERESTING);
- 总迭代次数与执行时长。
如果测试出现 FAILED,说明检测到被定义为 FORBIDDEN 的结果,表示存在潜在并发问题。
配置方式
通过在 build.gradle
中添加如下配置,可调整测试策略:
jcstress {
verbose = "true"
timeMillis = "200"
spinStyle = "THREAD_YIELD"
}
常用配置项说明:
配置项 | 说明 |
---|---|
mode |
执行模式:sanity 、quick 、default 、tough 、stress 。 |
cpuCount |
使用的 CPU 核心数,默认使用全部。 |
forks |
每个测试被 fork 的次数。 |
iterations |
每个测试的迭代次数。 |
heapPerFork |
每次 fork 的最大内存(MB)。 |
spinStyle |
等待策略:如 THREAD_YIELD 、HARD 等。 |
affinityMode |
线程与 CPU 的绑定模式:NONE 、GLOBAL 、LOCAL 。 |
jvmArgs/jvmArgsPrepend |
设置 JVM 启动参数。 |
reportDir |
报告输出目录。 |
如何编写 JCStress 测试
JCStress 提供两种测试风格,适用于不同的并发验证场景。
风格一:基于 Arbiter 的测试
特点是通过独立的 @Arbiter
方法观察最终状态:
- 定义共享变量;
- 用
@Actor
注解要并发执行的方法; - 用
@Arbiter
注解标注最终的观察方法; - 使用
@JCStressTest
+@Outcome
标注预期结果。
@JCStressTest
@Outcome(id = "1", expect = Expect.ACCEPTABLE, desc = "正常结果")
@Outcome(id = "0", expect = Expect.FORBIDDEN, desc = "出现竞态")
@State
public class ArbiterTest {
int x;
@Actor
void actor1() {
x = 1;
}
@Actor
void actor2() {
x = 1;
}
@Arbiter
void arbiter(I_Result r) {
r.r1 = x;
}
}
风格二:直接结果记录(Direct Result Reporting)
特点是 actor
方法直接记录测试结果并写入结果对象
- 定义共享变量;
@Actor
方法接收结果参数,如II_Result
;- 记录观察到的值;
- 使用
@Outcome
定义期望结果。
@JCStressTest
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "线程 2 看到了写入")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "线程 2 没看到写入")
@State
public class DirectReportingTest {
int x;
@Actor
void actor1() {
x = 1;
}
@Actor
void actor2(II_Result r) {
r.r1 = 0;
r.r2 = x;
}
}
可用
@Description
添加简要说明,需要避免过长字符串,否则可能会触发StringIndexOutOfBoundsException
。
风格选择
场景 | 推荐风格 |
---|---|
需要统一观察最终状态 | Arbiter 风格 |
Actor 可即时记录观察结果 | 直接记录风格 |
关注可见性或重排序问题 | 直接记录风格 |
线程安全测试示例
示例测试说明:
- 两个
@Actor
并发往缓存中放入两个不同 key(schema 和 table); @Arbiter
验证缓存中是否存在这两个实体;- 定义了三种结果:
- 2:两个都存在
- 1:只存在一个
- 0:都不存在
public class xxx {
// some entity to test.
@JCStressTest
@Outcome.Outcomes({
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Both put() calls succeeded; both entries are visible in the cache."),
@Outcome(id = "1", expect = Expect.FORBIDDEN, desc = "Only one entry is visible; potential visibility or atomicity issue."),
@Outcome(id = "0", expect = Expect.FORBIDDEN, desc = "Neither entry is visible; indicates a serious failure in write propagation or cache logic.")})
@Description("Concurrent put() on different keys. Both schema and table should be visible (result = 2). " + "Lower results may indicate visibility or concurrency issues.")
@State
public static class ConcurrentPutDifferentKeysTest {
private final EntityCache cache;
public ConcurrentPutDifferentKeysTest() {
this.cache = new CaffeineEntityCache(new Config() {
});
}
@Actor
public void actor1() {
cache.put(schemaEntity);
}
@Actor
public void actor2() {
cache.put(tableEntity);
}
@Arbiter
public void arbiter(I_Result r) {
int count = 0;
if (cache.contains(schemaEntity.nameIdentifier(), schemaEntity.type())) count++;
if (cache.contains(tableEntity.nameIdentifier(), tableEntity.type())) count++;
r.r1 = count;
}
}
// ... other test classes
}
JCStress 测试生命周期
JCStress 遵循以下测试执行流程:
- 状态初始化:每次测试迭代前重新构造
@State
对象,确保隔离性; - 并发执行:多个
@Actor
方法并发运行,模拟真实竞争; - 内存屏障插入:确保执行顺序后再执行
@Arbiter
; - 结果判定:将每次迭代的结果与定义的
@Outcome
比较归类; - 高频迭代:每个测试重复运行百万次以上,以暴露极低概率的错误。
可用执行模式
模式 | 描述 |
---|---|
sanity |
快速检查测试是否配置正确(几秒) |
quick |
快速反馈,适合开发中使用(几十秒) |
default |
平衡模式,推荐常规使用(几分钟) |
tough |
强力模式,适合 CI 测试或发布前验证(较长时间) |
问题类型
JCStress 主要帮助检测如下并发问题:
- 竞态条件(Race Condition):多个线程无同步访问共享变量;
- 可见性问题:线程修改对其他线程不可见;
- 原子性破坏:非原子操作被部分执行;
- 执行顺序错误:由于 CPU/JVM 重排序导致的行为异常。
Jcstress 编写建议
- 每个测试聚焦一个并发问题;
- 使用清晰的类名与注释解释每个结果;
- 避免使用非确定性行为(如
Thread.sleep()
); - 多次运行测试以验证稳定性;
- 对 FORBIDDEN 或 INTERESTING 的结果重点关注和修复。