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:

  1. 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.
  2. 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(); }
    }
  3. 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.