QA Interview Hub · База знань · Тести
Java: основи
Паралельність
ThreadLocal<WebDriver> і спільний стан у паралельних тестах
Паралельний запуск скорочує час прогону, але всі потоки одного JVM ділять статичні поля й об’єкти. Тому тест має володіти власним станом. Три рівні рішення:
- Нестатичні поля — коли раннер створює окремий екземпляр тестового класу для кожного тесту (JUnit 5 за замовчуванням). Для TestNG екземпляр один на клас, тому поле спільне для його методів.
- ThreadLocal — кожен потік бачить власне значення змінної:
public final class DriverHolder {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
public static WebDriver get() { return DRIVER.get(); }
public static void set(WebDriver d) { DRIVER.set(d); }
public static void quit() { DRIVER.get().quit(); DRIVER.remove(); }
}
- Окремі дані на тест — унікальні користувачі й сутності, а не спільний акаунт, інакше тести заважають один одному (змінили пароль, видалили товар).
Нюанси ThreadLocal:
remove() після тесту обов’язковий: пул повторно використовує потоки, і значення «просочиться» у наступний тест або утримуватиме пам’ять.
- Дочірній потік не бачить значення батьківського (для цього є
InheritableThreadLocal). Якщо тест сам запускає потоки, передавайте драйвер явно.
- ThreadLocal не робить сам WebDriver потокобезпечним: один драйвер усе одно використовує один потік.
Спільний змінюваний стан (лічильники, HashMap з кешем токенів, список результатів) спричиняє race condition — результат залежить від порядку потоків. Рішення: уникати спільного стану; ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger; або synchronized.
synchronized, volatile, atomic і ExecutorService
Чому count++ небезпечний. Це три кроки: прочитати, збільшити, записати. Два потоки можуть прочитати те саме значення, і одне збільшення загубиться. Варіанти:
AtomicInteger / AtomicLong: counter.incrementAndGet() — атомарно й без блокувань; найпростіший вибір для лічильників.
synchronized-метод чи блок: лише один потік за раз виконує ділянку коду під одним монітором. Дорожче, а при неправильному порядку захоплення кількох замків можливий deadlock.
volatile: гарантує, що зміни поля видно іншим потокам, але не робить складні операції (++) атомарними. Підходить для прапорця «зупинитись».
- Потокобезпечні колекції (
ConcurrentHashMap), а не HashMap/ArrayList зі спільним записом.
ExecutorService — пул потоків для власних паралельних задач у тестах (наприклад, навантажувальний запит із багатьох клієнтів, перевірка гонки двох замовлень):
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
List<Future<Response>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) futures.add(pool.submit(() -> client.createOrder(cart)));
for (Future<Response> f : futures) assertThat(f.get().status()).isEqualTo(201);
} finally {
pool.shutdown();
}
submit повертає Future; get() чекає результат і кидає ExecutionException з причиною, якщо задача впала. Виняток у потоці пулу інакше нікуди не потрапляє.
- Пул треба зупинити:
shutdown() (і awaitTermination), інакше потоки утримують JVM. Із Java 19 ExecutorService має close(), тож працює try-with-resources.
- Для
get задавайте таймаут: f.get(10, TimeUnit.SECONDS).
- Java 21 додала віртуальні потоки:
Executors.newVirtualThreadPerTaskExecutor() — зручно для багатьох блокуючих HTTP-викликів.