Multithreading lets a Java program do several things at once, which is exactly what happens when TestNG runs tests in parallel. This guide covers creating threads, the thread lifecycle, the race conditions that appear when threads share data, and how synchronized, volatile and atomic classes prevent them.

Threads — Creating & Managing

In Simple Terms

Threads = Workers in a restaurant:

Imagine one chef doing EVERYTHING: take order, cook, clean. That's single-threaded. Now imagine 3 chefs: one takes orders, one cooks, one cleans — all at the SAME TIME. That's multithreading! Each thread is an independent worker.

Creating Threads — 3 Ways

// ── METHOD 1: Extend Thread class ──
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread: " + Thread.currentThread().getName());
    }
}
MyThread t1 = new MyThread();
t1.start();  // starts new thread — DO NOT call run() directly!

// ── METHOD 2: Implement Runnable (preferred) ──
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable thread running");
    }
}
Thread t2 = new Thread(new MyTask());
t2.start();

// Lambda (Java 8+)
Thread t3 = new Thread(() -> System.out.println("Lambda thread"));
t3.start();

// ── METHOD 3: Callable + Future (returns result) ──
import java.util.concurrent.*;

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;  // returns a value!
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
System.out.println("Doing other work...");
int result = future.get();  // blocks until result ready: 42
executor.shutdown();

Thread Lifecycle

                  NEW
                   │  start()
                   ▼
    ┌─────────► RUNNABLE ◄──────────────┐
    │            │   │                  │
    │   scheduler│   │ not scheduled    │
    │            ▼   │                  │
    │          RUNNING                  │
    │            │                      │
    │  sleep()   │    notify()          │
    │  wait()    ▼    ──────────────────┤
    │          BLOCKED/WAITING          │
    │  (waiting for lock, I/O, sleep)   │
    │                                   │
    │  run() completes / exception      │
    └───────────────► TERMINATED        │

Thread t = new Thread(() -> { /* work */ });
t.getState();  // NEW
t.start();
t.getState();  // RUNNABLE (Java has no separate RUNNING state)
Thread.sleep(100);  // TIMED_WAITING
// after run() completes: TERMINATED

Note: the diagram shows RUNNING as a separate step for clarity, but Thread.getState() only reports NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED. A thread that is executing reports RUNNABLE.

Interview Questions

Difference between start() and run()?

start() creates a NEW thread and executes run() in it. Calling run() directly executes the code in the CURRENT thread — no new thread is created. Always use start() for multithreading.

Can we restart a stopped thread?

No. Once a thread reaches TERMINATED state, it cannot be restarted. Calling start() again throws IllegalThreadStateException. Create a new thread instance instead.

Advertisement

Synchronization & Thread Safety

The Problem — Race Condition

// UNSAFE: Two threads updating same counter
class Counter {
    int count = 0;
    void increment() { count++; }  // NOT atomic!
}
// count++ is 3 operations: READ → ADD → WRITE
// Thread 1 reads 5, Thread 2 reads 5
// Both write 6 — lost update! Should be 7

Solutions

// ── SOLUTION 1: synchronized method ──
class SafeCounter {
    int count = 0;

    public synchronized void increment() {
        count++;  // Only one thread can be here at a time
    }

    // ── SOLUTION 2: synchronized block (more granular) ──
    public void increment2() {
        synchronized (this) {  // lock on 'this' object
            count++;
        }
    }

    // ── SOLUTION 3: AtomicInteger (best for counters) ──
    AtomicInteger atomicCount = new AtomicInteger(0);
    public void incrementAtomic() {
        atomicCount.incrementAndGet();  // hardware-level atomic
    }
}

// ── SOLUTION 4: ReentrantLock (explicit lock) ──
import java.util.concurrent.locks.*;

class LockCounter {
    int count = 0;
    ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // ALWAYS unlock in finally!
        }
    }
}

Deep Dive: Monitor Locks & Object Header

// Every Java object has a 'monitor' (mutex lock)
// stored in the object header (mark word)

// synchronized(obj) means:
// 1. Thread tries to acquire obj's monitor
// 2. If free: thread owns it, enters block
// 3. If owned by another thread: current thread BLOCKS
// 4. On exit: monitor released, blocked threads can compete

// Reentrant: same thread can acquire same lock multiple times
synchronized (this) {
    synchronized (this) {  // same thread: OK! Count just increments
        // ...
    }
}

// wait() / notify() / notifyAll()
// Must be called inside synchronized block!
synchronized (lock) {
    while (!condition) {
        lock.wait();     // releases lock and waits
    }
    // do work
    lock.notifyAll();   // wake up all waiting threads
}

volatile keyword

// Without volatile: each thread may cache variable locally
// Changes by one thread not visible to others!

class Flag {
    // volatile: always read/write from MAIN MEMORY
    // Guarantees VISIBILITY (not atomicity!)
    volatile boolean running = true;

    public void stop() {
        running = false;  // visible to ALL threads immediately
    }

    public void run() {
        while (running) {  // sees updated value
            // do work
        }
    }
}
// volatile is enough for: boolean flags, singleton DCL
// NOT enough for: compound operations (count++, check-then-act)

Interview Questions

What is deadlock and how to avoid it?

Deadlock: Thread A holds Lock1, waits for Lock2. Thread B holds Lock2, waits for Lock1. Neither progresses. Prevention: always acquire locks in the same order, use tryLock() with timeout, minimize lock scope, avoid holding multiple locks.

Difference between synchronized and ReentrantLock?

synchronized: simpler, auto-releases on exception, no timeout. ReentrantLock: explicit lock/unlock, tryLock() with timeout, lockInterruptibly(), fairness option, multiple Conditions. Prefer synchronized for simple cases, ReentrantLock for advanced control.

What is the difference between volatile and synchronized?

volatile ensures visibility (changes seen by all threads) but not atomicity (compound operations not safe). synchronized ensures both visibility AND atomicity but with higher overhead. Use volatile for simple flags, synchronized/atomic for compound operations.

Thread Class — Complete Reference

// ── THREAD CREATION ──
Thread t = new Thread(() -> System.out.println("Running"), "MyThread");
t.setName("WorkerThread-1");
t.setPriority(Thread.MAX_PRIORITY); // 10
t.setPriority(Thread.MIN_PRIORITY); // 1
t.setPriority(Thread.NORM_PRIORITY); // 5 (default)
t.setDaemon(true); // daemon thread: JVM exits when only daemons left
t.start();

// ── THREAD INFO ──
t.getName()          // "WorkerThread-1"
t.getId()            // thread ID (long)
t.getPriority()      // 5
t.isDaemon()         // false
t.isAlive()          // true if started and not terminated
t.getState()         // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
t.getStackTrace()    // StackTraceElement[]
Thread.currentThread() // get current thread
Thread.activeCount() // approximate number of active threads

// ── SLEEP / YIELD / JOIN ──
Thread.sleep(1000);         // sleep 1 second (throws InterruptedException)
Thread.sleep(1000, 500000); // 1 second + 500000 nanoseconds
Thread.yield();             // hint scheduler to run other threads
t.join();                   // wait for t to finish
t.join(5000);               // wait max 5 seconds
t.join(5000, 500000);       // with nanoseconds

// ── INTERRUPT ──
t.interrupt();              // request interruption
t.isInterrupted();          // check if interrupted (does NOT clear flag)
Thread.interrupted();       // check AND clear interrupt flag (static)

// Responding to interruption:
Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        doWork();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // restore interrupt flag!
            break; // exit loop
        }
    }
});
worker.start();
Thread.sleep(1000);
worker.interrupt(); // stop worker

// ── UNCAUGHT EXCEPTION HANDLER ──
t.setUncaughtExceptionHandler((thread, ex) -> {
    System.err.println("Thread " + thread.getName() + " threw: " + ex);
    // log, alert, recover...
});

// Global default handler
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
    System.err.println("Uncaught in " + thread.getName() + ": " + ex);
});

// ── THREAD GROUPS ──
ThreadGroup group = new ThreadGroup("WorkerGroup");
Thread t1 = new Thread(group, () -> {}, "worker-1");
Thread t2 = new Thread(group, () -> {}, "worker-2");
t1.start(); t2.start();
group.activeCount()    // number of active threads
group.interrupt()      // interrupt all threads in group
group.list()           // print group info

// ── INHERITABLE THREAD LOCAL ──
// Child threads inherit parent's ThreadLocal values
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("parent-value");
new Thread(() -> {
    System.out.println(itl.get()); // "parent-value" (inherited!)
}).start();

// ── VIRTUAL THREADS (Java 21) ──
Thread vt = Thread.ofVirtual().name("virtual-1").start(() -> {
    System.out.println(Thread.currentThread().isVirtual()); // true
});
vt.join();