QA Interview Hub · Knowledge base · Quizzes
Java basics
Parallelism
ThreadLocal<WebDriver> and shared state in parallel tests
Parallel runs shorten a run, but all threads of one JVM share static fields and objects. So a test must own its state. Three levels of the solution:
- Instance fields — when the runner creates a separate instance of the test class for each test (JUnit 5 does by default). In TestNG there is one instance per class, so the field is shared by its methods.
- ThreadLocal — each thread sees its own value of the variable:
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(); }
}
- Separate data per test — unique users and entities instead of a shared account, otherwise tests interfere (one changed a password, another deleted an item).
ThreadLocal nuances:
remove() after a test is mandatory: a pool reuses threads, and the value would leak into the next test or hold memory.
- A child thread does not see the parent's value (
InheritableThreadLocal exists for that). If a test starts threads itself, pass the driver explicitly.
- ThreadLocal does not make WebDriver itself thread-safe: one driver is still used by one thread.
Shared mutable state (counters, a HashMap caching tokens, a list of results) causes a race condition — the result depends on thread order. Solutions: avoid shared state; ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger; or synchronized.
synchronized, volatile, atomic and ExecutorService
Why count++ is unsafe. It is three steps: read, increment, write. Two threads can read the same value and one increment is lost. Options:
AtomicInteger / AtomicLong: counter.incrementAndGet() — atomic and lock-free; the simplest choice for counters.
- A
synchronized method or block: only one thread at a time runs the section under one monitor. It is costlier, and acquiring several locks in the wrong order can cause a deadlock.
volatile: guarantees that changes of a field are visible to other threads, but does not make compound operations (++) atomic. Fits a "stop" flag.
- Thread-safe collections (
ConcurrentHashMap) instead of HashMap/ArrayList with shared writes.
ExecutorService is a thread pool for your own parallel tasks in tests (for example a request from many clients, checking a race between two orders):
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 returns a Future; get() waits for the result and throws ExecutionException with the cause if the task failed. An exception in a pool thread otherwise goes nowhere.
- The pool must be stopped:
shutdown() (and awaitTermination), otherwise its threads keep the JVM alive. Since Java 19 ExecutorService has close(), so try-with-resources works.
- Give
get a timeout: f.get(10, TimeUnit.SECONDS).
- Java 21 added virtual threads:
Executors.newVirtualThreadPerTaskExecutor() — handy for many blocking HTTP calls.