使用 Fray 检测 JVM 语言中的并发问题

  • 2025-12-11
    北京
  • 本文字数:2220 字

    阅读完需:约 7 分钟

卡内基梅隆大学推出了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 后,可以使用以下命令运行测试:

 

./gradlew frayTest
复制代码

 

最后,可以使用 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 代码并发问题的可选框架包括VMLensJava Concurrency Stress(jcstress)以及 IntelliJ IDEA 的Lincheck。要了解有关 Fray 的更多信息,请参阅使用指南技术报告

 

原文链接:

https://www.infoq.com/news/2025/12/fray-detects-concurrency-issues/