简介
jcstress:全名The Java Concurrency Stress tests,是一个实验工具和一套测试工具,用于帮助研究JVM、类库和硬件中并发支持的正确性。
官方github:https://github.com/openjdk/jcstress
官方给出的jcstress使用案例:http://hg.openjdk.java.net/code-tools/jcstress/file/tip/jcstress-samples/src/main/java/org/openjdk/jcstress/samples
依赖
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.5</version>
</dependency>
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-samples</artifactId>
<version>0.5</version>
</dependency>
HelloWorld
@JCStressTest
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Both actors came up with the same value: atomicity failure.")
@Outcome(id = "1, 2", expect = Expect.ACCEPTABLE, desc = "actor1 incremented, then actor2.")
@Outcome(id = "2, 1", expect = Expect.ACCEPTABLE, desc = "actor2 incremented, then actor1.")
@State
public class APISample_01_Simple {
int v;
@Actor
public void actor1(II_Result r) {
r.r1 = ++v; // record result from actor1 to field r1
}
@Actor
public void actor2(II_Result r) {
r.r2 = ++v; // record result from actor2 to field r2
}
}
是不是一脸懵逼?一堆不认识的注解,且听我细细道来。
@JCStressTest:
该注解标记一个类为一个并发测试的类,有一个属性 value 为Mode。mode 有Continuous 和 Termination 两种模式。Continuous 模式是运行几个 Actor 、Ariter 线程,并收集结果。Termination 模式运行具有阻塞/循环操作的单个 Actor 和 Singal 的测试。
@Outcome:
描述测试结果,并处理这个结果,该注解有 id、expect、desc 这三个属性。其中 id 接收结果,id 还支持正则表达式;expect 是期望处理结果,有 ACCEPTABLE、ACCEPTABLE_INTERESTING 、FORBIDDEN、UNKNOWN 四种类型,ACCEPTABLE 表示接受当前结果,ACCEPTABLE 结果不一定会存在;ACCEPTABLE_INTERESTING 和 ACCEPTABLE 差不多,唯一不一样的是,这个结果会在生成的报告中高亮;FORBIDDEN 表示永远不应该出现的结果,若测试过程中有该结果,意味着测试失败; UNKNOWN 没有评分,不使用。
@State:
标记这个类是有状态的,有状态的意识是拥有数据,而且数据是可以被修改的,如上面测试例子中的 v , 其中 v 就是拥有的数据。State 修饰的类必须是 public 的,不能是内部类,但是可以是静态内部类,如上面例子。State 修饰的类必须有一个默认构造函数
@Actor:
该注解标记的方法会被线程调用,被 Actor 修饰方法所在的类必须有 State 或者 Result注解,被其修饰的方法可以抛出异常,但是抛出异常的话,会引起测试失败。注意的是,Actor 标记的每个方法仅由一个特定线程调用,而且每个被 State 标记的实例仅调用每一个方法 。Actor 修饰的方法之间是没有顺序的,调用是并发执行的。
那么如何运行呢?并没有发现有运行的入口。
配置idea:
添加一个application
这里配置需要注意:main class必须写成org.openjdk.jcstress.Main
,此外需要配置包名 -t package path
,package path具体和你的测试类所在的包一样。
配置完成后,运行,等待一小段时间。
运行成功后,在我们项目根路径下生成一个results文件夹,打开里面的index.html可以查看测试结果
@Result
在上一个案例中,我们使用II_Result类去收集两个线程返回的结果(int类型),并在Outcome注解的id属性中列举了所有可能出现的情况。那我们如果想要收益一个int的返回值,或者收集float、char、byte等其他基本类型和Object类型,应该用什么类?
实际上jcstress已经在org.openjdk.jcstress.infra.results包下列举了很多返回值类型供我们在平时开发测试中使用。
![image-20210121222106002](https://gitee.com/wj204811/wj204811/raw/master/img/20210121222106.png)
我们可以使用@Result注解自定义返回值类型,如下图,该注解标记的类所有 field 都必须是原生数据类似或者是 String 类型,所有 field 都应该是 publi。不过需要自定义的情况几乎没有
@Arbiter
Arbiter 注解和 Actor 注解差不多,不一样的是 Arbiter 注解声明的方法运行在所有 Actor之后,而且 Actor 所有的内存都对 Arbiter 可见,这就使得 Arbiter 在确认最终状态信息上有很大的作用
@JCStressTest
// These are the test outcomes.
@Outcome(id = "1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "One update lost: atomicity failure.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Actors updated independently.")
@State
public class APISample_02_Arbiters {
int v;
@Actor
public void actor1() {
v++;
}
@Actor
public void actor2() {
v++;
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = v;
}
}
@Signal
一些并发测试没有遵循Mode.Continuous。一个有趣的测试组之一是断言代码是否在一个信号。
在这里,我们使用一个@Actor,等待一个字段,以及一个@Signal设置该字段。JCStress会启动actor,然后传递信号。
如果在合理时间内退出,则记录“TERMINATED”结果,否则记录“STALE”。
public class APISample_03_Termination {
@JCStressTest(Mode.Termination)
@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE, desc = "Gracefully finished.")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Test hung up.")
@State
public static class NotHasVolatile{
int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}
@JCStressTest(Mode.Termination)
@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE, desc = "Gracefully finished.")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Test hung up.")
@State
public static class HasVolatile{
volatile int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}
}
有volatile关键字:
无volatile关键字:
测试结果符合预期。
@ JCStressMeta元数据共享
Description 、Outcome、Ref 这些注解是可以放到一个公共类,然后由@ JCStressMeta 注解引进来,以达到重复使用的目的。
@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE, desc = "Gracefully finished.")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Test hung up.")
public class APISample_05_SharedMetadata {
@JCStressTest(Mode.Termination)
@JCStressMeta(APISample_05_SharedMetadata.class)
@State
public static class NotHasVolatile{
int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}
@JCStressTest(Mode.Termination)
@JCStressMeta(APISample_05_SharedMetadata.class)
@State
public static class HasVolatile{
volatile int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}
}
Descriptions描述信息
使用@Description和@Ref可以描述测试信息。只需要在测试类的上标注即可,对实际测试结果并无影响。
实战:AtomicInteger原子性测试
@JCStressTest
@Outcome(id = "1", expect = Expect.FORBIDDEN, desc = "One update lost.")
@Outcome(id = "2", expect = Expect.ACCEPTABLE, desc = "Both updates.")
@State
public class AtomicIncrementTest {
AtomicInteger ai = new AtomicInteger();
@Actor
public void actor1() {
ai.incrementAndGet();
}
@Actor
public void actor2() {
ai.incrementAndGet();
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = ai.get();
}
}
并发测试结果:
由图中结果Both updates,可知AtomicInteger的incrementAndGet具有原子性。