卡内基梅隆大学推出了Fray,这是一个面向 JVM 程序的并发测试工具,可以用于捕获和重放错误。Fray 是基于这篇研究论文用 Kotlin 编写的。它不能找出所有的并发问题,但能利用最新的研究成果最大化检测它们的机会。Fray 使用了影子锁定技术,通过额外的锁以特定顺序协调对共享资源的访问。
Fray 支持包括 JDK 25 在内的 Java 版本,并已成功发现JDK、Lucene、Kafka、Flink 和 Guava 中的 Bug。该框架能够检测多线程问题,但不能检测由并发内存写入引起的错误。Maven 需要以下插件和依赖配置:
<plugin> <groupId>org.pastalab.fray.maven</groupId> <artifactId>fray-plugins-maven</artifactId> <version>0.6.9</version> <executions> <execution> <id>prepare-fray</id> <goals> <goal>prepare-fray</goal> </goals> </execution> </executions></plugin><dependency> <groupId>org.pastalab.fray</groupId> <artifactId>fray-junit</artifactId> <version>0.6.9</version> <scope>test</scope></dependency>
复制代码
或者,在 Gradle 中使用以下插件配置:
plugins { id("org.pastalab.fray.gradle") version "0.6.9"}
复制代码
配置好 Gradle 后,可以使用以下命令运行测试:
最后,可以使用 IntelliJ IDEA 运行 Fray 测试,具体方法请参考 GitHub 上提供的IDE文档。
配置好构建系统后,可以使用 JUnit 5 运行测试,通过在类上使用注解 @ExtendWith(FrayTestExtension.class),在测试上使用注解 @ConcurrencyTest:
@ExtendWith(FrayTestExtension.class)public class MyFirstTest { @ConcurrencyTest public void myTest() { … }}
复制代码
下面的 BankAccount 类是一个简化的示例,当多个线程访问同步代码时可能会导致死锁:
public class BankAccount { public BankAccount(double balance) { this.balance = balance; } private double balance; public void transfer(double amount, BankAccount toAccount) { synchronized (this) { synchronized (toAccount) { this.balance -= amount; toAccount.balance += amount; } } }}
复制代码
为了检测死锁,我们创建了一个 Fray 测试,并显式设置迭代次数为 10。要了解完整的参数集,可以查看 GitHub 上的ConcurrencyTest.kt文件。
@ExtendWith(FrayTestExtension.class)public class BankAccountTest { public void myBankAccountTest() throws InterruptedException { BankAccount bankAccount1 = new BankAccount(5000); BankAccount bankAccount2 = new BankAccount(6000); Thread thread1 = new Thread(() -> { bankAccount1.transfer(100, bankAccount2); }); Thread thread2 = new Thread(() -> { bankAccount2.transfer(50, bankAccount1); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } @ConcurrencyTest(iterations = 10) public void runMyBankAccountTestUsingFray() throws InterruptedException { myBankAccountTest(); }}
复制代码
运行上述测试,4 次(共 10 次)迭代后出现以下错误:
[ERROR] Errors: [ERROR] org.example.BankAccountTest.runMyBankAccountTestUsingFray [INFO] Run 1: PASS [INFO] Run 2: PASS [INFO] Run 3: PASS [ERROR] Run 4: BankAccountTest.runMyBankAccountTestUsingFray:33->myBankAccountTest:25->Object.wait:-1 » Deadlock [INFO] Run 5: PASS [INFO] Run 6: PASS [INFO] Run 7: PASS [INFO] Run 8: PASS [INFO] Run 9: PASS [INFO] Run 10: PASS
或者,对于 JUnit 之外的其他测试框架,可以使用 FrayInTestLauncher 类:
public void myTest() { FrayInTestLauncher.INSTANCE.launchFrayTest(() -> { … });}
复制代码
Fray 会在测试失败时自动生成一个测试用例用于重现失败过程。详细信息记录在报告文件夹中。使用 Maven 时,报告文件夹位于 target/fray/fray-report 目录内。使用这个文件夹可以通过两种不同的方式重现失败过程。
第一种方案是使用相同的调度器和记录的随机选择重新运行测试,具体方法详见论文:分布式系统设计的反馈引导自适应测试。因此,应在 ConcurrencyTest 注解中设置重放文件的路径:
@ConcurrencyTest( replay = "[path to report]/recording")
复制代码
立即重新运行测试,出现以下错误:
Error: org.pastalab.fray.runtime.DeadlockException
复制代码
在示例应用程序内部解决死锁后,测试通过。
第二种方法是使用从原执行中观察到的精确线程调度再次执行测试。因此,需要使用以下 Java 选项记录调度信息:-Dfray.recordSchedule=true。记录调度信息后,应在 ConcurrencyTest 注解中使用 ReplayScheduler 类:
@ConcurrencyTest( scheduler = ReplayScheduler.class, replay = "[path to report]/recording")
复制代码
其他可用于检测 Java 代码并发问题的可选框架包括VMLens、Java Concurrency Stress(jcstress)以及 IntelliJ IDEA 的Lincheck。要了解有关 Fray 的更多信息,请参阅使用指南或技术报告。
原文链接:
https://www.infoq.com/news/2025/12/fray-detects-concurrency-issues/
评论