Команда Spring АйО подготовила перевод про Fray — инструмент для обнаружения и воспроизведения ошибок многопоточности в Java-программах. Основанный на научных исследованиях и написанный на Kotlin, Fray использует технику теневой блокировки для выявления взаимоблокировок и других проблем синхронизации. Он уже доказал свою эффективность на таких проектах, как Kafka, Flink и Lucene.
Комментарий от Михаила Поливаха
В экосистеме Java уже довольно долгое время существуют решения, которые позволяют тестировать concurrent сценарии, например JCStress от разработчиков OpenJDK или Lincheck от JetBrains.
Важно то, что, как заявляется, Fray способен не просто выявлять баги в concurrency, но и воспроизводить их, а также помочь в разъяснении причин багов, а это very huge deal.
Баги в многопоточности они тем и неприятны, что они плавающие, трудно воспроизводимые и т.д. Если Fray позволит не просто обнаружить баг, но и отдебажить его, то он явно найдет спрос в Enterprise.
Университет Карнеги-Меллона представил Fray — инструмент для тестирования конкурентности в программах на JVM, предназначенный для выявления ошибок и их воспроизведения. Написанный на Kotlin и основанный на научной работе, Fray не способен обнаружить все проблемы конкурентности, но использует новейшие исследования для максимизации вероятности их выявления. Fray применяет технику теневой блокировки (shadow locking), которая вводит дополнительные блокировки для упорядоченного доступа к разделяемым ресурсам.
Fray поддерживает Java, включая версию JDK 25, и успешно выявил ошибки в JDK, Lucene, Kafka, Flink и Guava. Этот фреймворк способен обнаруживать проблемы многопоточности, но не ошибки, вызванные конкурентными записями в память. Для использования с 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
Наконец, для запуска тестов Fray можно использовать IntelliJ, следуя документации IDE на GitHub.
После настройки системы сборки можно использовать 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, в котором явно задано количество итераций — десять. Полный набор параметров доступен в файле ConcurrencyTest.kt на GitHub.
@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();
}
}
Запуск приведённого выше теста приводит к следующей ошибке после первых четырёх (из десяти) итераций:
[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 автоматически сгенерирует test case при сбое теста для воспроизведения ошибки. Подробная информация сохраняется в папке отчётов. При использовании Maven папка отчётов располагается по пути target/fray/fray-report. Эту папку можно использовать для воспроизведения сбоя двумя способами.
Первый способ — повторный запуск теста с тем же планировщиком и зафиксированными случайными выборами, как описано в статье Feedback-guided Adaptive Testing of Distributed Systems Designs. Для этого путь к файлу воспроизведения необходимо указать в аннотации 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) и Lincheck от IntelliJ IDEA. Дополнительную информацию о Fray можно найти в руководстве по использованию или в техническом отчёте.
Комментарий от Павла Кислова
Данная статья является кратким обзором для первого знакомства и не дает ответов на все вопросы. Более того, быстрых ответов в данной теме нет. Однако, для терпеливого и любопытного читателя, все необходимое есть возможность изучить подробно в приложенных материалах:
Гитхаб с документацией проекта: https://github.com/cmu-pasta/fray/blob/main/docs/usage.md#1-replay-using-recorded-random-choices
Детальная документация с описанием решаемых проблем: Feedback-guided Adaptive Testing of Distributed Systems Designs

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.