β˜• Java

Java Multithreading β€” Syntax, Types, Examples & Best Practices

Everything you need to know about Java Multithreading β€” Thread class, Runnable interface, thread lifecycle, synchronization, volatile keyword, wait/notify, Executor framework, thread pools, Callable/Future, CompletableFuture, concurrent collections, atomic classes, ReentrantLock, deadlock prevention, and virtual threads (Java 21).

πŸ“…

Last Updated

March 2026

⏱️

Read Time

30 min

🎯

Level

Advanced

🏷️

Chapter

28 of 35

What is Multithreading in Java?

Multithreading is the ability of a Java program to execute multiple threads simultaneously within a single process. A thread is the smallest unit of CPU execution β€” it has its own program counter, call stack, and local variables, but shares the heap memory (objects, static fields, open files) with all other threads in the same JVM process. This shared memory is what makes threads powerful and dangerous at the same time.

Modern applications are inherently concurrent. A web server handles thousands of HTTP requests simultaneously β€” each on its own thread. A payment gateway processes multiple transactions in parallel. A trading platform ticks market prices while executing orders concurrently. An IDE compiles code in the background while you keep typing. Without multithreading, none of these systems would scale.

The two primary benefits of multithreading are: (1) CPU utilisation β€” on a multi-core processor, threads run truly in parallel across cores, multiplying throughput. (2) Responsiveness β€” the UI thread stays free while a background thread does heavy computation or waits for I/O. The primary risk is shared mutable state β€” when two threads simultaneously read and write the same object without coordination, the result is unpredictable: race conditions, stale data, and corruption.

Creating Threads β€” Thread Class vs Runnable Interface

Java provides two classic ways to define a concurrent task: extending Thread and implementing Runnable. A third way β€” implementing Callable β€” is used when the task must return a result or throw a checked exception. In modern Java, lambdas make Runnable and Callable extremely concise, and the Executor framework manages the actual threads β€” raw Thread creation is reserved for low-level scenarios.

1️⃣
Extending Thread (least preferred)

class MyTask extends Thread { public void run() { /* task */ } } new MyTask().start(); Problems: (1) Consumes the single Java inheritance slot β€” MyTask can't extend any other class. (2) Tightly couples task logic with thread lifecycle. (3) Cannot be submitted to an Executor. Use only if you need to override other Thread methods (e.g., custom interrupt handling).

2️⃣
Implementing Runnable (preferred)

class MyTask implements Runnable { public void run() { /* task */ } } new Thread(new MyTask()).start(); // OR submit to executor: executor.submit(new MyTask()); // OR lambda: executor.submit(() -> { /* task */ }); Advantages: separates task from thread, allows extending another class, compatible with all Executor/thread pool APIs.

3️⃣
Implementing Callable (for results)

class MyTask implements Callable<Integer> { public Integer call() throws Exception { return 42; } } Future<Integer> f = executor.submit(new MyTask()); int result = f.get(); // blocks until done Callable differs from Runnable: (1) returns a typed result, (2) can throw checked exceptions, (3) used with Future/CompletableFuture.

β˜• JavaCreatingThreads.java β€” All Three Approaches
import java.util.concurrent.*;

// ── Approach 1: Extend Thread (avoid in production) ──────────────
class DownloadTask extends Thread {
    private final String url;
    DownloadTask(String url) { this.url = url; }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()
            + " downloading: " + url);
        // ... download logic
    }
}

// ── Approach 2: Implement Runnable (preferred) ───────────────────
class ReportGenerator implements Runnable {
    private final String reportType;
    ReportGenerator(String type) { this.reportType = type; }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()
            + " generating " + reportType + " report");
        // ... report generation logic
    }
}

// ── Approach 3: Implement Callable (returns result) ──────────────
class PriceCalculator implements Callable<Double> {
    private final String productId;
    PrimeCalculator(String id) { this.productId = id; }

    @Override
    public Double call() throws Exception {
        // ... fetch price from DB
        System.out.println("Calculating price for: " + productId);
        return 1499.99;
    }
}

public class CreatingThreads {
    public static void main(String[] args) throws Exception {

        // Approach 1: direct thread start
        Thread t1 = new DownloadTask("https://example.com/file.zip");
        t1.setName("Download-Thread");
        t1.start(); // start(), NOT run() β€” run() is synchronous!

        // Approach 2: Runnable via Thread
        Thread t2 = new Thread(new ReportGenerator("Sales"), "Report-Thread");
        t2.start();

        // Approach 2: Runnable via Lambda (cleanest for simple tasks)
        Thread t3 = new Thread(() ->
            System.out.println(Thread.currentThread().getName() + " processing"),
            "Lambda-Thread");
        t3.start();

        // Approach 3: Callable via ExecutorService
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Future<Double> priceFuture = executor.submit(new PriceCalculator("PROD-001"));

        // Do other work while price is being calculated...
        System.out.println("Main thread doing other work...");

        Double price = priceFuture.get(); // blocks until result is ready
        System.out.println("Price fetched: β‚Ή" + price);

        executor.shutdown();
    }
}

Thread Lifecycle β€” States & Transitions

A Java thread passes through a well-defined set of states during its lifetime, defined by the Thread.State enum. Understanding these states is essential for debugging stuck threads, analysing thread dumps, and understanding what transitions are possible.

StateThread.State EnumDescriptionHow to Enter
NewNEWThread created but not yet startednew Thread(runnable)
RunnableRUNNABLEEligible to run β€” may be waiting for CPU turnthread.start()
Running(subset of RUNNABLE)Actively executing on a CPU coreJVM scheduler selects it
BlockedBLOCKEDWaiting to acquire a monitor lock held by another threadContended synchronized block entry
WaitingWAITINGWaiting indefinitely for another thread to signalwait(), join(), LockSupport.park()
Timed WaitingTIMED_WAITINGWaiting with a timeout β€” wakes automaticallysleep(ms), wait(ms), join(ms), tryLock(timeout)
TerminatedTERMINATEDrun() has returned or threw an uncaught exceptionrun() completes normally or with exception
β˜• JavaThreadLifecycle.java β€” Observing State Transitions
public class ThreadLifecycle {
    public static void main(String[] args) throws InterruptedException {

        Object lock = new Object();

        Thread worker = new Thread(() -> {
            try {
                System.out.println("Worker: starting work");
                Thread.sleep(500);           // TIMED_WAITING

                synchronized (lock) {
                    lock.wait(300);          // WAITING (on lock)
                }

                System.out.println("Worker: finishing");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("Worker: interrupted");
            }
        }, "WorkerThread");

        System.out.println("State after new     : " + worker.getState()); // NEW

        worker.start();
        System.out.println("State after start() : " + worker.getState()); // RUNNABLE

        Thread.sleep(100); // let worker enter sleep
        System.out.println("State during sleep  : " + worker.getState()); // TIMED_WAITING

        Thread.sleep(600); // let worker enter wait
        System.out.println("State during wait   : " + worker.getState()); // WAITING or TIMED_WAITING

        worker.join();     // wait for completion
        System.out.println("State after join    : " + worker.getState()); // TERMINATED

        // ── Daemon threads β€” JVM exits without waiting for them ──
        Thread daemon = new Thread(() -> {
            while (true) {
                try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
                System.out.println("Daemon heartbeat");
            }
        });
        daemon.setDaemon(true); // MUST set before start()
        daemon.start();
        // JVM will terminate daemon thread automatically when main finishes
    }
}

Important Thread Methods β€” sleep, join, interrupt, yield

The Thread class provides several key methods for controlling thread execution, coordination between threads, and graceful shutdown. Understanding their precise semantics β€” especially the difference between sleep and wait, and the correct way to handle interrupt β€” is essential for writing correct concurrent code.

MethodEffectReleases Lock?Key Note
Thread.sleep(ms)Current thread pauses for ms millisecondsNO β€” holds all locksAlways handle InterruptedException; re-interrupt the thread
thread.join()Calling thread waits until 'thread' terminatesNOjoin(ms) adds a timeout; use in main to wait for workers
thread.interrupt()Sets the interrupt flag; wakes sleeping/waiting threadsN/ASleeping threads throw InterruptedException; always re-interrupt
Thread.yield()Hints scheduler to give up CPU turnNONon-binding hint β€” JVM may ignore it; rarely used in production
obj.wait()Releases lock on obj, thread waitsYES β€” releases lockMust be inside synchronized(obj); wait for notify/notifyAll
obj.notify()Wakes ONE thread waiting on objNO β€” still holds lockJVM chooses which waiting thread to wake β€” unpredictable
obj.notifyAll()Wakes ALL threads waiting on objNO β€” still holds lockSafer than notify() β€” woken threads re-compete for lock
Thread.currentThread()Returns reference to the executing threadN/AgetName(), getId(), getState(), isInterrupted()
β˜• JavaThreadMethods.java
import java.util.concurrent.atomic.AtomicBoolean;

public class ThreadMethods {

    // ── Graceful shutdown using interrupt flag ────────────────────
    static class DataPoller implements Runnable {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("Polling data...");
                    Thread.sleep(1000); // reacts to interrupt immediately
                } catch (InterruptedException e) {
                    // βœ… Re-interrupt ALWAYS β€” never swallow InterruptedException
                    Thread.currentThread().interrupt();
                    System.out.println("Poller: interrupt received β€” shutting down");
                    break;
                }
            }
            System.out.println("Poller: clean shutdown complete");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // ── Thread.sleep() ─────────────────────────────────────
        System.out.println("Main: sleeping 500ms");
        Thread.sleep(500);
        System.out.println("Main: awake");

        // ── thread.join() β€” wait for completion ───────────────
        Thread[] workers = new Thread[3];
        for (int i = 0; i < 3; i++) {
            final int id = i;
            workers[i] = new Thread(() -> {
                try {
                    Thread.sleep((long)(Math.random() * 1000));
                    System.out.println("Worker-" + id + " done");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "Worker-" + i);
            workers[i].start();
        }
        // Wait for ALL workers to complete
        for (Thread w : workers) w.join();
        System.out.println("Main: all workers finished");

        // ── Graceful interrupt-based shutdown ────────────────
        Thread poller = new Thread(new DataPoller(), "DataPoller");
        poller.start();
        Thread.sleep(3500); // let it poll 3 times
        poller.interrupt(); // signal shutdown
        poller.join();      // wait for clean exit
        System.out.println("Main: poller shut down gracefully");
    }
}

Synchronization β€” Preventing Race Conditions

A race condition occurs when two or more threads access shared mutable data concurrently and the outcome depends on the unpredictable order of their execution. Java's primary tool to prevent race conditions is synchronization β€” built around the concept of a monitor lock (also called an intrinsic lock). Every Java object has exactly one monitor lock. The synchronized keyword causes a thread to acquire the lock before entering a block or method, and release it upon exit β€” ensuring only one thread at a time executes the guarded code.

πŸ”’
synchronized Method

public synchronized void deposit(double amount) { ... } Acquires lock on 'this' (instance methods) or on the Class object (static methods). Entire method is the critical section. Simple to write but coarser-grained β€” any caller must wait even if it only needs to read an unrelated field. Two synchronized instance methods on the SAME object cannot run concurrently.

πŸ”
synchronized Block (preferred)

public void deposit(double amount) { // non-critical work here (concurrent) synchronized (this) { balance += amount; // critical section } // non-critical work here (concurrent) } Finer-grained: only the minimum code that accesses shared state is locked. You can choose any object as the lock β€” enabling independent locks for independent data.

⚑
Lock Objects for Fine-Grained Control

private final Object balanceLock = new Object(); private final Object historyLock = new Object(); synchronized (balanceLock) { balance += amount; } synchronized (historyLock) { history.add(txn); } Balance updates and history updates can now run concurrently β€” they use different locks. This technique maximises throughput in high-contention scenarios.

β˜• JavaSynchronization.java β€” Race Condition vs Synchronized
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// ── BAD: Unsynchronized counter β€” race condition ──────────────────
class UnsafeCounter {
    private int count = 0;
    public void increment() { count++; } // NOT atomic: read β†’ increment β†’ write
    public int  getCount()  { return count; }
}

// ── GOOD: Synchronized counter ────────────────────────────────────
class SafeCounter {
    private int count = 0;

    public synchronized void increment() { count++; }
    public synchronized int  getCount()  { return count; }
}

// ── BETTER: Synchronized block with private lock ──────────────────
class BankAccount {
    private double balance;
    private final Object lock = new Object(); // private lock object

    public BankAccount(double initialBalance) { this.balance = initialBalance; }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        synchronized (lock) {          // lock on private object, not 'this'
            balance += amount;
        }
    }

    public boolean withdraw(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        synchronized (lock) {
            if (balance < amount) return false;
            balance -= amount;
            return true;
        }
    }

    public double getBalance() {
        synchronized (lock) { return balance; }
    }
}

public class SynchronizationDemo {
    public static void main(String[] args) throws InterruptedException {

        int threadCount  = 100;
        int incPerThread = 1000;

        // ── Race condition demo ─────────────────────────────────
        UnsafeCounter unsafe = new UnsafeCounter();
        ExecutorService pool1 = Executors.newFixedThreadPool(10);
        for (int i = 0; i < threadCount; i++)
            pool1.submit(() -> { for (int j = 0; j < incPerThread; j++) unsafe.increment(); });
        pool1.shutdown(); pool1.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("Unsafe expected: " + (threadCount * incPerThread));
        System.out.println("Unsafe actual  : " + unsafe.getCount()); // likely < 100000!

        // ── Synchronized counter ────────────────────────────────
        SafeCounter safe = new SafeCounter();
        ExecutorService pool2 = Executors.newFixedThreadPool(10);
        for (int i = 0; i < threadCount; i++)
            pool2.submit(() -> { for (int j = 0; j < incPerThread; j++) safe.increment(); });
        pool2.shutdown(); pool2.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("Safe expected  : " + (threadCount * incPerThread));
        System.out.println("Safe actual    : " + safe.getCount()); // always 100000
    }
}

volatile Keyword β€” Visibility Guarantee Without Mutual Exclusion

The volatile keyword solves a specific problem: memory visibility. Without volatile, each CPU core may cache a thread's local copy of a variable in its registers or L1/L2 cache. A write by Thread A may not be visible to Thread B running on a different core for an unpredictable amount of time. volatile guarantees that every read of a volatile variable goes to main memory, and every write immediately flushes to main memory β€” establishing a happens-before relationship.

Propertyvolatilesynchronized
Visibilityβœ… Guarantees all threads see the latest writeβœ… Guarantees visibility on lock acquisition
Mutual Exclusion❌ NO β€” multiple threads can read/write simultaneouslyβœ… YES β€” only one thread in critical section at a time
AtomicityOnly for single read/write of long, double, referencesβœ… Entire synchronized block is atomic
PerformanceLow overhead β€” no lock acquisition/releaseHigher overhead β€” lock contention possible
Use caseSingle-variable flag or state indicatorCompound actions (check-then-act, read-modify-write)
count++ atomic?❌ NO β€” volatile int count; count++ is NOT atomicβœ… YES inside synchronized block
β˜• JavaVolatileKeyword.java
public class VolatileKeyword {

    // ── Correct use of volatile: single boolean flag ─────────────
    static class BackgroundService implements Runnable {
        private volatile boolean running = true; // visibility guarantee

        @Override
        public void run() {
            System.out.println("Service started");
            while (running) { // always reads from main memory
                try {
                    processNextItem();
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            System.out.println("Service stopped gracefully");
        }

        public void stop() {
            running = false; // write immediately flushed to main memory
        }

        private void processNextItem() {
            System.out.println("Processing...");
        }
    }

    // ── WITHOUT volatile β€” the JVM may CACHE 'running' in a register
    // The while loop could run forever even after stop() is called!

    // ── volatile does NOT fix compound operations ─────────────────
    static volatile int counter = 0;

    static void badIncrement() {
        counter++; // ❌ NOT atomic even with volatile
        // Expands to: read counter β†’ increment β†’ write counter
        // Two threads can both read same value before either writes
    }

    static synchronized void goodIncrement() {
        counter++; // βœ… synchronized makes compound action atomic
    }

    // ── Double-checked locking β€” classic volatile use ─────────────
    // WITHOUT volatile, broken due to instruction reordering
    private static volatile BackgroundService instance;

    public static BackgroundService getInstance() {
        if (instance == null) {                    // first check (no lock)
            synchronized (VolatileKeyword.class) {
                if (instance == null) {            // second check (with lock)
                    instance = new BackgroundService(); // volatile write
                }
            }
        }
        return instance; // volatile read β€” always sees fully initialised object
    }

    public static void main(String[] args) throws InterruptedException {
        BackgroundService svc = new BackgroundService();
        Thread t = new Thread(svc, "BackgroundService");
        t.start();
        Thread.sleep(500);
        svc.stop(); // write to volatile β€” service sees it immediately
        t.join();
    }
}

wait(), notify(), notifyAll() β€” Thread Communication

While synchronized prevents two threads from running simultaneously, wait() and notify() allow threads to coordinate β€” one thread can pause and wait for a condition to be met, while another thread signals that the condition has changed. This is the classic Producer-Consumer coordination pattern. All three methods (wait, notify, notifyAll) must be called inside a synchronized block on the same object β€” the monitor object.

β˜• JavaWaitNotify.java β€” Producer-Consumer with Bounded Buffer
import java.util.LinkedList;
import java.util.Queue;

public class WaitNotify {

    // ── Bounded buffer shared between producer and consumer ───────
    static class BoundedBuffer<T> {
        private final Queue<T> queue;
        private final int      capacity;
        private final Object   lock = new Object();

        BoundedBuffer(int capacity) {
            this.capacity = capacity;
            this.queue    = new LinkedList<>();
        }

        // Producer calls this β€” blocks if buffer is full
        public void put(T item) throws InterruptedException {
            synchronized (lock) {
                while (queue.size() == capacity) { // βœ… while loop, not if
                    System.out.println("[Producer] Buffer full β€” waiting");
                    lock.wait(); // releases lock; waits for consumer signal
                }
                queue.offer(item);
                System.out.println("[Producer] Produced: " + item
                    + " | Buffer: " + queue.size() + "/" + capacity);
                lock.notifyAll(); // wake consumers β€” item is available
            }
        }

        // Consumer calls this β€” blocks if buffer is empty
        public T take() throws InterruptedException {
            synchronized (lock) {
                while (queue.isEmpty()) { // βœ… while loop, not if
                    System.out.println("[Consumer] Buffer empty β€” waiting");
                    lock.wait(); // releases lock; waits for producer signal
                }
                T item = queue.poll();
                System.out.println("[Consumer] Consumed: " + item
                    + " | Buffer: " + queue.size() + "/" + capacity);
                lock.notifyAll(); // wake producers β€” space is available
                return item;
            }
        }
    }

    public static void main(String[] args) {
        BoundedBuffer<Integer> buffer = new BoundedBuffer<>(3);

        // ── Producer thread ───────────────────────────────────────
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 8; i++) {
                    buffer.put(i);
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Producer");

        // ── Consumer thread ───────────────────────────────────────
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 8; i++) {
                    buffer.take();
                    Thread.sleep(500); // consumer is slower than producer
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Consumer");

        producer.start();
        consumer.start();
    }
}

Executor Framework β€” Thread Pools Done Right

Creating a raw new Thread() for every task is dangerous in production: thread creation is expensive (1–10ms), unbounded thread creation exhausts OS resources, and there is no lifecycle management. The Executor framework (java.util.concurrent) solves all of this β€” it manages a pool of reusable threads, queues tasks, handles thread lifecycle, and provides controlled shutdown. ExecutorService is the interface; Executors factory class creates common pool configurations.

Factory MethodThread Pool TypeBest ForRisk
Executors.newFixedThreadPool(n)n threads, unbounded queueCPU-bound tasks β€” n = number of CPU coresTasks queue up if n is too small
Executors.newCachedThreadPool()Grows as needed, idle threads removed after 60sShort-lived async tasks, I/O-boundUnbounded thread creation under load
Executors.newSingleThreadExecutor()1 thread, unbounded queueSequential task processing, ordered executionSingle point of failure
Executors.newScheduledThreadPool(n)n threads for delayed/periodic tasksScheduled jobs, cron-style tasksComplex lifecycle management
new ThreadPoolExecutor(…) [recommended]Full control over all parametersProduction use β€” tunable core/max/queueRequires careful configuration
Executors.newVirtualThreadPerTaskExecutor()Virtual thread per task (Java 21)Massive I/O-bound concurrencyPinning issues with synchronized
β˜• JavaExecutorFramework.java
import java.util.concurrent.*;

public class ExecutorFramework {
    public static void main(String[] args) throws Exception {

        // ── Fixed Thread Pool β€” for CPU-bound tasks ───────────────
        int cores = Runtime.getRuntime().availableProcessors();
        ExecutorService cpuPool = Executors.newFixedThreadPool(cores);

        for (int i = 1; i <= 8; i++) {
            final int taskId = i;
            cpuPool.submit(() -> {
                System.out.printf("Task %d running on %s%n",
                    taskId, Thread.currentThread().getName());
                // simulate CPU work
                double result = 0;
                for (int j = 0; j < 1_000_000; j++) result += Math.sqrt(j);
                System.out.printf("Task %d done%n", taskId);
            });
        }
        cpuPool.shutdown();
        cpuPool.awaitTermination(60, TimeUnit.SECONDS);

        // ── Custom ThreadPoolExecutor β€” production recommended ────
        ThreadPoolExecutor customPool = new ThreadPoolExecutor(
            4,                              // corePoolSize
            8,                              // maximumPoolSize
            60L, TimeUnit.SECONDS,          // keepAliveTime for extra threads
            new ArrayBlockingQueue<>(100),  // bounded task queue
            new ThreadFactory() {           // named threads for debugging
                int counter = 1;
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "CustomPool-" + counter++);
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy() // back-pressure strategy
        );

        // Submit tasks
        for (int i = 1; i <= 5; i++) {
            final int id = i;
            customPool.submit(() ->
                System.out.println(Thread.currentThread().getName() + ": task " + id));
        }

        customPool.shutdown();
        if (!customPool.awaitTermination(30, TimeUnit.SECONDS)) {
            customPool.shutdownNow(); // force shutdown if tasks take too long
        }

        // ── ScheduledExecutorService β€” periodic tasks ─────────────
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

        // Run once after 2 second delay
        scheduler.schedule(() -> System.out.println("Delayed task!"),
            2, TimeUnit.SECONDS);

        // Run every 1 second, starting after 0 delay
        ScheduledFuture<?> periodic = scheduler.scheduleAtFixedRate(
            () -> System.out.println("Periodic: " + System.currentTimeMillis()),
            0, 1, TimeUnit.SECONDS);

        Thread.sleep(4000);
        periodic.cancel(false); // stop periodic task
        scheduler.shutdown();
    }
}

Callable & Future β€” Threads That Return Results

Runnable.run() returns void and cannot throw checked exceptions β€” it provides no way to get a result back from a thread. Callable<V> solves both problems: its call() method returns a typed value V and can throw any checked exception. When submitted to an ExecutorService, it returns a Future<V> β€” a handle to the computation that lets you check completion, retrieve the result, cancel the task, or handle exceptions.

β˜• JavaCallableFuture.java β€” Parallel Price Aggregator
import java.util.concurrent.*;
import java.util.*;

public class CallableFuture {

    // ── Callable that fetches a product price ─────────────────────
    static class PriceFetcher implements Callable<Double> {
        private final String productId;
        private final String source;

        PriceFetcher(String productId, String source) {
            this.productId = productId;
            this.source    = source;
        }

        @Override
        public Double call() throws Exception {
            System.out.printf("[%s] Fetching price for %s...%n",
                source, productId);
            Thread.sleep((long)(Math.random() * 1000)); // simulate network
            double price = 1000 + Math.random() * 500;
            System.out.printf("[%s] Price for %s: β‚Ή%.2f%n",
                source, productId, price);
            return price;
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        // ── Submit multiple Callables, get Futures ────────────────
        String productId = "LAPTOP-XPS15";
        List<Future<Double>> futures = new ArrayList<>();
        futures.add(executor.submit(new PriceFetcher(productId, "Amazon")));
        futures.add(executor.submit(new PriceFetcher(productId, "Flipkart")));
        futures.add(executor.submit(new PriceFetcher(productId, "Croma")));

        System.out.println("Main: waiting for all prices...");

        // ── Collect results ────────────────────────────────────────
        double minPrice = Double.MAX_VALUE;
        for (Future<Double> future : futures) {
            try {
                double price = future.get(5, TimeUnit.SECONDS); // timeout!
                minPrice = Math.min(minPrice, price);
            } catch (TimeoutException e) {
                System.out.println("Price fetch timed out");
                future.cancel(true); // interrupt the blocked thread
            } catch (ExecutionException e) {
                // Callable threw an exception β€” wrapped in ExecutionException
                System.out.println("Price fetch failed: " + e.getCause().getMessage());
            }
        }
        System.out.printf("Best price: β‚Ή%.2f%n", minPrice);

        // ── invokeAll β€” submit all, wait for all to complete ───────
        List<Callable<Double>> tasks = List.of(
            new PriceFetcher("PHONE-S24", "Amazon"),
            new PriceFetcher("PHONE-S24", "Flipkart"),
            new PriceFetcher("PHONE-S24", "Reliance")
        );
        List<Future<Double>> allResults = executor.invokeAll(tasks);
        System.out.println("All prices fetched: " + allResults.size());

        // ── invokeAny β€” return as soon as ONE succeeds ────────────
        double firstPrice = executor.invokeAny(tasks);
        System.out.printf("First available price: β‚Ή%.2f%n", firstPrice);

        executor.shutdown();
    }
}

CompletableFuture β€” Non-Blocking Async Pipelines (Java 8+)

CompletableFuture<T> (Java 8+) is the most powerful concurrency API in modern Java. Unlike Future β€” which blocks when you call get() β€” CompletableFuture lets you build non-blocking async pipelines: chain transformations, combine multiple futures, handle errors, and define callbacks that execute when results become available β€” all without blocking any thread.

β˜• JavaCompletableFutureDemo.java β€” Async Order Processing Pipeline
import java.util.concurrent.*;

public class CompletableFutureDemo {

    // Simulated async services
    static CompletableFuture<String> fetchUser(String userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(300);
            return "User:" + userId;
        });
    }

    static CompletableFuture<Double> fetchAccountBalance(String userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(200);
            return 50000.0;
        });
    }

    static CompletableFuture<String> processPayment(double amount, String userId) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(400);
            if (amount > 100000) throw new RuntimeException("Amount exceeds daily limit");
            return "TXN-" + System.currentTimeMillis();
        });
    }

    static CompletableFuture<Void> sendConfirmation(String txnId, String userId) {
        return CompletableFuture.runAsync(() -> {
            sleep(100);
            System.out.println("Confirmation sent for " + txnId + " to " + userId);
        });
    }

    public static void main(String[] args) throws Exception {

        // ── Sequential pipeline β€” each step waits for previous ────
        String userId = "USR-1001";
        double amount = 5000.0;

        CompletableFuture<Void> pipeline = fetchUser(userId)
            .thenCompose(user -> processPayment(amount, user)) // flat map
            .thenCompose(txnId -> sendConfirmation(txnId, userId))
            .exceptionally(ex -> {
                System.out.println("Pipeline failed: " + ex.getMessage());
                return null;
            });

        pipeline.join(); // wait for completion

        // ── Parallel execution β€” combine independent futures ───────
        CompletableFuture<String> userFuture    = fetchUser(userId);
        CompletableFuture<Double> balanceFuture = fetchAccountBalance(userId);

        // Both run in parallel β€” thenCombine waits for both
        CompletableFuture<String> combined = userFuture.thenCombine(
            balanceFuture,
            (user, balance) -> user + " has balance β‚Ή" + balance
        );
        System.out.println(combined.get()); // User:USR-1001 has balance β‚Ή50000.0

        // ── allOf β€” wait for ALL futures ──────────────────────────
        CompletableFuture<String>[] tasks = new CompletableFuture[]{
            fetchUser("USR-001"), fetchUser("USR-002"), fetchUser("USR-003")
        };
        CompletableFuture.allOf(tasks).join();
        System.out.println("All users fetched");

        // ── anyOf β€” return as soon as FIRST completes ─────────────
        CompletableFuture<Object> firstDone = CompletableFuture.anyOf(
            fetchUser("USR-A"), fetchUser("USR-B"), fetchUser("USR-C")
        );
        System.out.println("First result: " + firstDone.get());

        // ── Error handling ────────────────────────────────────────
        CompletableFuture<String> withFallback =
            processPayment(200000, userId) // will throw
            .exceptionally(ex -> "FAILED:" + ex.getMessage())
            .thenApply(result -> "Result: " + result);
        System.out.println(withFallback.get());
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Concurrent Collections β€” Thread-Safe Data Structures

Standard Java collections (ArrayList, HashMap, HashSet) are not thread-safe. Concurrent access without synchronisation causes corruption, ConcurrentModificationException, and infinite loops. The java.util.concurrent package provides production-ready thread-safe collections designed for high concurrency β€” far more efficient than wrapping everything in Collections.synchronizedXxx().

CollectionThread-Safe AlternativeKey Advantage
ArrayListCopyOnWriteArrayListReads never block; writes copy the entire array β€” ideal for read-heavy, rare-write scenarios
HashMapConcurrentHashMapSegment-level locking β€” reads never block; 16Γ— more concurrent than Hashtable
HashSetConcurrentHashMap.newKeySet()Built on ConcurrentHashMap; thread-safe Set operations
LinkedList (queue)LinkedBlockingQueueBlocking put/take β€” producer-consumer without manual wait/notify
PriorityQueuePriorityBlockingQueueThread-safe priority ordering with blocking take()
ArrayDequeArrayBlockingQueueBounded blocking queue β€” provides backpressure
LinkedList (deque)LinkedBlockingDequeThread-safe double-ended queue with blocking operations
TreeMapConcurrentSkipListMapConcurrent sorted map β€” O(log n) with full thread safety
β˜• JavaConcurrentCollections.java
import java.util.concurrent.*;
import java.util.*;

public class ConcurrentCollections {
    public static void main(String[] args) throws InterruptedException {

        // ── ConcurrentHashMap β€” high-performance thread-safe map ──
        ConcurrentHashMap<String, Integer> wordCount = new ConcurrentHashMap<>();

        String[] words = {"java", "thread", "java", "concurrent", "thread", "java"};
        // Thread-safe merge β€” no manual synchronization needed
        for (String word : words) {
            wordCount.merge(word, 1, Integer::sum);
        }
        System.out.println("Word counts: " + wordCount);

        // putIfAbsent β€” atomic check-then-insert (no synchronized needed)
        wordCount.putIfAbsent("kotlin", 0);
        // computeIfAbsent β€” create value lazily
        wordCount.computeIfAbsent("scala", k -> 0);

        // ── LinkedBlockingQueue β€” producer-consumer ───────────────
        LinkedBlockingQueue<String> taskQueue = new LinkedBlockingQueue<>(10);

        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    taskQueue.put("Task-" + i); // blocks if full
                    System.out.println("Enqueued Task-" + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }, "Producer");

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    String task = taskQueue.take(); // blocks if empty
                    System.out.println("Processing: " + task);
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }, "Consumer");

        producer.start(); consumer.start();
        producer.join();  consumer.join();

        // ── CopyOnWriteArrayList β€” read-heavy scenarios ───────────
        CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
        listeners.add("EmailNotifier");
        listeners.add("SmsNotifier");

        // Safe to iterate while another thread modifies:
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.submit(() -> listeners.add("PushNotifier"));
        // This iteration won't throw ConcurrentModificationException
        listeners.forEach(l -> System.out.println("Listener: " + l));
        pool.shutdown();
    }
}

Atomic Classes β€” Lock-Free Thread Safety

The java.util.concurrent.atomic package provides thread-safe classes for single-variable operations without locks. They use Compare-And-Swap (CAS) β€” a CPU-level instruction that atomically reads, compares, and writes a value only if it still matches the expected value. This is faster than synchronised methods for simple operations because there is no lock contention, no context switch, and no thread blocking.

β˜• JavaAtomicClasses.java
import java.util.concurrent.atomic.*;
import java.util.concurrent.*;

public class AtomicClasses {
    public static void main(String[] args) throws InterruptedException {

        int threads   = 100;
        int perThread = 1000;

        // ── AtomicInteger β€” lock-free int operations ──────────────
        AtomicInteger atomicCounter = new AtomicInteger(0);

        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < threads; i++)
            pool.submit(() -> {
                for (int j = 0; j < perThread; j++)
                    atomicCounter.incrementAndGet(); // atomic: CAS loop
            });
        pool.shutdown(); pool.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("AtomicInteger: " + atomicCounter.get()); // always 100000

        // Key atomic methods:
        AtomicInteger ai = new AtomicInteger(10);
        System.out.println(ai.getAndIncrement());   // 10 (returns OLD value)
        System.out.println(ai.incrementAndGet());   // 12 (returns NEW value)
        System.out.println(ai.addAndGet(5));        // 17
        System.out.println(ai.compareAndSet(17, 20)); // true β€” atomically set to 20
        System.out.println(ai.compareAndSet(17, 99)); // false β€” value is 20, not 17
        System.out.println(ai.get());               // 20

        // ── AtomicLong β€” for large counters (request count, bytes) ─
        AtomicLong requestCount = new AtomicLong(0L);
        requestCount.incrementAndGet();

        // ── AtomicBoolean β€” for flags ─────────────────────────────
        AtomicBoolean isInitialised = new AtomicBoolean(false);
        // Atomic check-and-set β€” only one thread will see true
        if (isInitialised.compareAndSet(false, true)) {
            System.out.println("This thread performed initialisation");
        }

        // ── AtomicReference β€” for any object ──────────────────────
        AtomicReference<String> currentConfig = new AtomicReference<>("v1.0");
        String old = currentConfig.getAndSet("v2.0");
        System.out.println("Updated from " + old + " to " + currentConfig.get());

        // ── LongAdder β€” better than AtomicLong for high-contention counters ─
        // Distributes updates across multiple cells; aggregates on sum()
        LongAdder adder = new LongAdder();
        ExecutorService pool2 = Executors.newFixedThreadPool(10);
        for (int i = 0; i < threads; i++)
            pool2.submit(() -> {
                for (int j = 0; j < perThread; j++) adder.increment();
            });
        pool2.shutdown(); pool2.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println("LongAdder sum: " + adder.sum()); // always 100000
        // LongAdder significantly outperforms AtomicLong under heavy contention
    }
}

ReentrantLock, ReadWriteLock & Semaphore

java.util.concurrent.locks provides explicit lock classes that offer capabilities impossible with synchronized: tryLock with timeout (avoid deadlock), interruptible lock acquisition, fairness policies, separate read and write locks, and Condition objects for fine-grained thread coordination. These are power tools β€” use them when synchronized cannot meet the requirements.

β˜• JavaLocksAndSemaphores.java
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class LocksAndSemaphores {

    // ── ReentrantLock β€” explicit lock with tryLock ────────────────
    static class ThreadSafeCache {
        private final java.util.Map<String, String> cache = new java.util.HashMap<>();
        private final ReentrantLock lock = new ReentrantLock(true); // fair mode

        public void put(String key, String value) throws InterruptedException {
            // tryLock with timeout β€” avoids deadlock
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
                    cache.put(key, value);
                } finally {
                    lock.unlock(); // ALWAYS unlock in finally
                }
            } else {
                System.out.println("Could not acquire lock β€” skipping put");
            }
        }

        public String get(String key) throws InterruptedException {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try { return cache.get(key); }
                finally { lock.unlock(); }
            }
            return null;
        }
    }

    // ── ReadWriteLock β€” concurrent reads, exclusive writes ─────────
    static class ConfigStore {
        private final java.util.Map<String, String> config = new java.util.HashMap<>();
        private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        private final Lock readLock  = rwLock.readLock();
        private final Lock writeLock = rwLock.writeLock();

        // Multiple threads can read simultaneously
        public String get(String key) {
            readLock.lock();
            try { return config.get(key); }
            finally { readLock.unlock(); }
        }

        // Only ONE thread can write at a time; blocks all readers
        public void set(String key, String value) {
            writeLock.lock();
            try { config.put(key, value); }
            finally { writeLock.unlock(); }
        }
    }

    // ── Semaphore β€” limit concurrent access (connection pool) ─────
    static class DatabaseConnectionPool {
        private final Semaphore semaphore;
        private final int maxConnections;

        DatabaseConnectionPool(int maxConnections) {
            this.maxConnections = maxConnections;
            this.semaphore = new Semaphore(maxConnections, true); // fair
        }

        public void executeQuery(String query, String threadName)
                throws InterruptedException {
            System.out.println(threadName + ": waiting for connection...");
            semaphore.acquire(); // blocks until a permit is available
            try {
                System.out.println(threadName + ": acquired connection ("
                    + (maxConnections - semaphore.availablePermits()) + " in use)");
                Thread.sleep(500); // simulate query execution
                System.out.println(threadName + ": query done");
            } finally {
                semaphore.release(); // ALWAYS release in finally
                System.out.println(threadName + ": released connection");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        DatabaseConnectionPool pool = new DatabaseConnectionPool(3);
        ExecutorService exec = Executors.newFixedThreadPool(7);
        for (int i = 1; i <= 7; i++) {
            final String name = "Thread-" + i;
            exec.submit(() -> {
                try { pool.executeQuery("SELECT ...", name); }
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            });
        }
        exec.shutdown();
        exec.awaitTermination(30, TimeUnit.SECONDS);
    }
}

Deadlock β€” Detection, Prevention & Resolution

A deadlock is one of the most feared concurrency bugs β€” a situation where two or more threads are permanently blocked, each waiting for a resource held by another. The JVM does not automatically detect or resolve deadlocks (unlike databases). A deadlocked application simply hangs β€” no error, no log, no recovery β€” making it extremely hard to diagnose in production.

πŸ”
Four Conditions for Deadlock (Coffman)

ALL four must hold simultaneously for deadlock to occur: 1. Mutual Exclusion β€” resources can only be held by one thread. 2. Hold and Wait β€” a thread holds one resource while waiting for another. 3. No Preemption β€” a resource cannot be forcibly taken from a thread. 4. Circular Wait β€” Thread A waits for Thread B, Thread B waits for Thread A. Breaking ANY one condition prevents deadlock.

πŸ›‘οΈ
Prevention Strategy 1: Lock Ordering

Always acquire multiple locks in the SAME GLOBAL ORDER across ALL threads. Bad: Thread A acquires Lock1 then Lock2; Thread B acquires Lock2 then Lock1. Good: Both threads always acquire Lock1 first, then Lock2. This breaks Circular Wait β€” deadlock is impossible when all threads agree on lock order. For locks on domain objects, use a consistent ordering like object hash code or ID.

⏱️
Prevention Strategy 2: tryLock with Timeout

Use ReentrantLock.tryLock(timeout) instead of synchronized or lock.lock(). If the lock isn't available within the timeout, back off, release all held locks, and retry later. This breaks Hold-and-Wait β€” a thread that can't get all locks releases what it has. Java thread dumps (jstack) and VisualVM show BLOCKED threads β€” useful for detecting deadlock in production.

β˜• JavaDeadlockDemo.java β€” Deadlock and Its Fix
import java.util.concurrent.locks.*;
import java.util.concurrent.TimeUnit;

// ── Classic Deadlock ──────────────────────────────────────────────
class Account {
    private final int id;
    private double balance;

    Account(int id, double balance) {
        this.id = id; this.balance = balance;
    }

    // ❌ DEADLOCK RISK: lock order depends on argument order
    public static void transferUnsafe(Account from, Account to, double amount)
            throws InterruptedException {
        synchronized (from) {
            Thread.sleep(50); // simulate work β€” increases chance of deadlock
            synchronized (to) {
                from.balance -= amount;
                to.balance   += amount;
            }
        }
    }

    // βœ… FIX 1: Consistent lock ordering by account ID
    public static void transferSafe(Account from, Account to, double amount) {
        Account first  = from.id < to.id ? from : to;
        Account second = from.id < to.id ? to   : from;
        synchronized (first) {
            synchronized (second) {
                from.balance -= amount;
                to.balance   += amount;
                System.out.printf("Transferred ₹%.0f: Acc%d→Acc%d%n",
                    amount, from.id, to.id);
            }
        }
    }

    // βœ… FIX 2: tryLock with timeout β€” no indefinite blocking
    private final ReentrantLock lock = new ReentrantLock();

    public static boolean transferWithTimeout(Account from, Account to,
            double amount) throws InterruptedException {
        boolean fromLocked = false, toLocked = false;
        try {
            fromLocked = from.lock.tryLock(500, TimeUnit.MILLISECONDS);
            toLocked   = to.lock.tryLock(500, TimeUnit.MILLISECONDS);
            if (fromLocked && toLocked) {
                from.balance -= amount;
                to.balance   += amount;
                System.out.printf("tryLock transfer: ₹%.0f Acc%d→Acc%d%n",
                    amount, from.id, to.id);
                return true;
            }
            System.out.println("Could not acquire both locks β€” will retry");
            return false;
        } finally {
            // ALWAYS release in finally, in reverse order
            if (toLocked)   to.lock.unlock();
            if (fromLocked) from.lock.unlock();
        }
    }

    public int    getId()      { return id; }
    public double getBalance() { return balance; }
}

public class DeadlockDemo {
    public static void main(String[] args) throws InterruptedException {
        Account acc1 = new Account(1, 100000);
        Account acc2 = new Account(2, 50000);

        // Safe transfer β€” no deadlock possible
        Thread t1 = new Thread(() -> {
            try { Account.transferSafe(acc1, acc2, 5000); }
            catch (Exception e) { e.printStackTrace(); }
        });
        Thread t2 = new Thread(() -> {
            try { Account.transferSafe(acc2, acc1, 3000); }
            catch (Exception e) { e.printStackTrace(); }
        });
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("Acc1: β‚Ή" + acc1.getBalance());
        System.out.println("Acc2: β‚Ή" + acc2.getBalance());
    }
}

Virtual Threads β€” Project Loom (Java 21)

Virtual threads are the most significant concurrency feature added to Java in two decades, standardised in Java 21 (JEP 444). Traditional platform threads map 1:1 to OS threads β€” each consumes roughly 1–2 MB of stack memory and is expensive to create and context-switch. Applications serving 10,000 concurrent connections needed 10,000 OS threads, which is impractical. Virtual threads are managed entirely by the JVM β€” they are mounted on a small pool of underlying OS threads (carrier threads) and automatically unmounted when they block on I/O, freeing the carrier thread for another virtual thread.

πŸ‹οΈ
Platform Thread (Traditional)

Backed by an OS thread. Stack: 512KB – 2MB per thread. OS thread limit: typically ~thousands per JVM. Creation: slow (~1ms, system call required). Blocking I/O: OS thread is blocked β€” carrier thread occupied. Cost: expensive β€” thread pool required to manage lifecycle. Best for: CPU-bound tasks, low-concurrency scenarios.

πŸͺΆ
Virtual Thread (Java 21+)

Backed by JVM-managed carrier threads (ForkJoinPool). Stack: starts at ~KB, grows lazily on heap. Capacity: MILLIONS per JVM. Creation: fast β€” no OS system call. Blocking I/O: virtual thread unmounted from carrier β€” carrier free for others. Cost: cheap β€” create one per task (no pooling needed). Best for: I/O-bound tasks, high-concurrency server applications.

⚠️
Virtual Thread Caveats

1. Pinning: a virtual thread is 'pinned' to its carrier if it holds a synchronized lock during I/O β€” blocking the carrier. Fix: replace synchronized with ReentrantLock inside code that does blocking I/O. 2. Thread-local variables: ThreadLocal with large values multiplied by millions of virtual threads = memory pressure. Consider alternatives. 3. Not for CPU-bound work: virtual threads don't add CPU parallelism β€” use ForkJoinPool/parallelStream for parallel computation. 4. Still in adaptation: many libraries need updates to use non-pinning I/O.

β˜• JavaVirtualThreads.java β€” Java 21
import java.util.concurrent.*;

public class VirtualThreads {
    public static void main(String[] args) throws Exception {

        // ── Creating virtual threads ──────────────────────────────

        // Method 1: Thread.ofVirtual()
        Thread vt1 = Thread.ofVirtual()
            .name("virtual-1")
            .start(() -> System.out.println(
                Thread.currentThread() + " is virtual: " +
                Thread.currentThread().isVirtual()));
        vt1.join();

        // Method 2: Thread.startVirtualThread() β€” simplest
        Thread vt2 = Thread.startVirtualThread(() ->
            System.out.println("Quick virtual thread: " +
                Thread.currentThread().isVirtual())); // true
        vt2.join();

        // Method 3: ExecutorService with virtual thread per task
        // βœ… Recommended for server applications
        try (ExecutorService executor =
                Executors.newVirtualThreadPerTaskExecutor()) {

            int requestCount = 10_000; // simulate 10K concurrent requests
            CountDownLatch latch = new CountDownLatch(requestCount);

            long start = System.currentTimeMillis();
            for (int i = 0; i < requestCount; i++) {
                final int reqId = i;
                executor.submit(() -> {
                    try {
                        // Simulate I/O-bound work (DB query, HTTP call)
                        Thread.sleep(100); // virtual thread unmounts here!
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        latch.countDown();
                    }
                });
            }
            latch.await();
            long elapsed = System.currentTimeMillis() - start;
            System.out.printf("%d tasks completed in %d ms%n",
                requestCount, elapsed);
            // With virtual threads: ~100ms (all run concurrently)
            // With 10-thread pool: ~100_000ms (sequential batches)
        }

        // ── Structured Concurrency (Java 21 Preview) ─────────────
        // Groups virtual threads that belong to the same logical task
        // try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        //     Future<String> user  = scope.fork(() -> fetchUser("U1"));
        //     Future<Double> price = scope.fork(() -> fetchPrice("P1"));
        //     scope.join().throwIfFailed();
        //     process(user.resultNow(), price.resultNow());
        // }
    }
}

Common Mistakes & Pitfalls β€” Bugs That Fool Everyone

Concurrency bugs are among the most insidious in software engineering β€” they are often non-deterministic, appear only under load, and vanish when you add logging or a debugger. These are the patterns that fill production incident reports.

β˜• JavaConcurrencyMistakes.java
// ❌ MISTAKE 1: Calling run() instead of start()
Thread t = new Thread(() -> System.out.println("Task"));
t.run();   // ❌ runs on CURRENT thread β€” no new thread created!
t.start(); // βœ… creates new OS thread, schedules it

// ❌ MISTAKE 2: Swallowing InterruptedException
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* ❌ NEVER do this β€” interrupt lost */ }
// Fix:
try { Thread.sleep(1000); }
catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // βœ… re-interrupt
    return; // or rethrow
}

// ❌ MISTAKE 3: Non-atomic check-then-act on shared state
// Broken even if 'count' is volatile:
if (count < MAX) {  // Thread A reads count=9
    count++;         // Thread B also reads count=9 β†’ both increment β†’ count=11!
}
// Fix: use synchronized block or AtomicInteger.compareAndSet()

// ❌ MISTAKE 4: Locking on a non-final object
// lock reference can change between threads β€” different objects locked!
private String lockObj = "lock"; // ❌ String interning makes this complex
private final Object LOCK = new Object(); // βœ… always final, always same object

// ❌ MISTAKE 5: Creating new Thread per request in production
// server.onRequest(req -> new Thread(() -> handle(req)).start());
// ❌ Under 10,000 concurrent requests: 10,000 OS threads β†’ OOM
// Fix: submit to ExecutorService or use virtual threads

// ❌ MISTAKE 6: Using HashMap in concurrent context
Map<String, Integer> map = new HashMap<>();
// Multiple threads writing β†’ ConcurrentModificationException, infinite loop!
// Fix: ConcurrentHashMap
Map<String, Integer> safe = new ConcurrentHashMap<>();

// ❌ MISTAKE 7: Not shutting down ExecutorService β†’ JVM hangs
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> System.out.println("task"));
// Missing pool.shutdown() β†’ non-daemon threads keep JVM alive forever!
pool.shutdown(); // βœ… always shutdown
pool.awaitTermination(30, TimeUnit.SECONDS); // βœ… wait for completion

// ❌ MISTAKE 8: Waiting in synchronized block without while loop
synchronized (obj) {
    if (queue.isEmpty()) obj.wait(); // ❌ if β€” spurious wakeup proceeds!
    process(queue.poll()); // may execute on empty queue
}
// Fix: while loop
synchronized (obj) {
    while (queue.isEmpty()) obj.wait(); // βœ… recheck condition every wakeup
    process(queue.poll());
}

Bad Practices & Anti-Patterns β€” What Senior Developers Reject

These concurrency anti-patterns are the most common causes of production incidents, performance degradation, and system instability in Java applications.

🚫
Thread-Per-Request Without Limits (Thread Bomb)

Spawning a new thread for every incoming request without any limit β€” whether via raw Thread creation or an unbounded CachedThreadPool β€” will exhaust OS thread limits under load. Each OS thread costs 512KB–2MB of stack. 10,000 requests = 10,000 threads = 10GB RAM for stacks alone. Use a bounded thread pool (ThreadPoolExecutor with bounded queue and CallerRunsPolicy) or virtual threads. Always design for the worst-case request concurrency.

🚫
Excessive Synchronization (Lock Granularity Too Coarse)

Synchronising entire methods or large blocks when only a few lines access shared state serialises all callers, eliminating concurrency benefits. Example: synchronizing a method that does 1000ms of computation to protect a 1ms counter update. Fix: use fine-grained locks covering only the critical section; use read/write locks when reads vastly outnumber writes; use atomic classes for single-variable updates.

🚫
Ignoring Thread Pool Saturation

Submitting tasks to an executor without handling RejectedExecutionException or ignoring task queue depth. When the queue fills and all threads are busy, the default AbortPolicy silently discards tasks. In a payment or order system, this causes silent data loss. Always choose a RejectionPolicy deliberately, monitor queue depth with pool.getQueue().size(), and alert or back-pressure when saturation is detected.

🚫
Using Deprecated Thread Methods (stop, suspend, resume)

Thread.stop() forcibly kills a thread while it holds locks β€” leaving shared objects in inconsistent state. Thread.suspend() holds locks while suspended β€” classic deadlock trigger. Both are deprecated since Java 1.2. Never use them. Use volatile flags, interrupt(), and cooperative shutdown patterns instead. The Java specification explicitly warns that these methods are fundamentally unsafe.

🚫
Sharing ThreadLocal State Across a Thread Pool

ThreadLocal stores per-thread data. In a thread pool, threads are reused β€” a ThreadLocal value set by one task is visible to the next task running on the same thread if it isn't cleared. This causes request context leakage: user A's authentication context leaking into user B's request is a security incident. Always call ThreadLocal.remove() in a finally block after the task completes when using thread pools.

🚫
Busy-Waiting (Polling in a Tight Loop)

while (!ready) {} β€” a busy-wait loop burns 100% CPU on a core doing nothing useful, and due to memory visibility rules, may loop forever without volatile. Use proper blocking mechanisms: CountDownLatch.await(), BlockingQueue.take(), CompletableFuture.join(), or wait()/notify(). Busy-waiting is only ever justified in very tight latency-sensitive lock-free algorithms, and even then with Thread.onSpinWait() hints to the CPU.

Real-World Production Code Examples β€” Multithreading in Context

The following examples model concurrency patterns used in real enterprise Java applications β€” patterns you will recognise from Spring Boot microservices, payment gateways, and high-throughput APIs.

β˜• JavaAsyncOrderProcessor.java β€” Parallel Service Aggregation
package com.techsustainify.order;

import java.util.concurrent.*;

public class AsyncOrderProcessor {

    private final ExecutorService executor;

    public AsyncOrderProcessor(int threadPoolSize) {
        this.executor = new ThreadPoolExecutor(
            threadPoolSize, threadPoolSize * 2,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(500),
            r -> { Thread t = new Thread(r); t.setName("OrderProcessor-" +
                   t.getId()); t.setDaemon(true); return t; },
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    // Process order: validate user, check stock, process payment β€” in parallel
    public CompletableFuture<OrderResult> processOrder(OrderRequest request) {

        CompletableFuture<UserValidation> userCheck =
            CompletableFuture.supplyAsync((),
                () -> validateUser(request.getUserId()), executor);

        CompletableFuture<StockReservation> stockCheck =
            CompletableFuture.supplyAsync(
                () -> reserveStock(request.getProductId(), request.getQty()), executor);

        // Wait for BOTH user validation and stock to succeed
        return userCheck.thenCombine(stockCheck, (user, stock) -> {
            if (!user.isValid())   throw new OrderException("User not eligible");
            if (!stock.isReserved()) throw new OrderException("Out of stock");
            return new PaymentRequest(user, stock, request.getAmount());
        })
        .thenCompose(payReq ->
            // Only after both pass β€” process payment async
            CompletableFuture.supplyAsync(
                () -> processPayment(payReq), executor))
        .thenApply(txn -> OrderResult.success(txn.getTransactionId()))
        .exceptionally(ex -> {
            // Rollback stock if payment failed
            rollbackStock(request.getProductId(), request.getQty());
            return OrderResult.failure(ex.getMessage());
        });
    }

    private UserValidation  validateUser(String userId)     { /* ... */ return null; }
    private StockReservation reserveStock(String pid, int qty) { /* ... */ return null; }
    private Transaction      processPayment(PaymentRequest r) { /* ... */ return null; }
    private void             rollbackStock(String pid, int qty) { /* ... */ }

    public void shutdown() throws InterruptedException {
        executor.shutdown();
        if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }
}
β˜• JavaRateLimiter.java β€” Semaphore-Based API Rate Limiter
package com.techsustainify.ratelimit;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Token-bucket rate limiter β€” limits concurrent API calls per client.
 * Uses Semaphore to enforce maximum concurrent requests.
 */
public class RateLimiter {

    private final Semaphore permits;
    private final int       maxConcurrent;
    private final long      timeoutMs;
    private final AtomicLong totalRequests  = new AtomicLong(0);
    private final AtomicLong rejectedRequests = new AtomicLong(0);

    public RateLimiter(int maxConcurrent, long timeoutMs) {
        this.maxConcurrent = maxConcurrent;
        this.timeoutMs     = timeoutMs;
        this.permits       = new Semaphore(maxConcurrent, true); // fair
    }

    public <T> T execute(Callable<T> task) throws Exception {
        totalRequests.incrementAndGet();

        boolean acquired = permits.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS);
        if (!acquired) {
            rejectedRequests.incrementAndGet();
            throw new RateLimitExceededException(
                "Rate limit exceeded. Max concurrent: " + maxConcurrent);
        }

        try {
            return task.call();
        } finally {
            permits.release(); // ALWAYS release β€” even on exception
        }
    }

    public int    getAvailablePermits()   { return permits.availablePermits(); }
    public long   getTotalRequests()       { return totalRequests.get(); }
    public long   getRejectedRequests()    { return rejectedRequests.get(); }
    public double getRejectionRate()       {
        long total = totalRequests.get();
        return total == 0 ? 0 : (double) rejectedRequests.get() / total * 100;
    }

    public static class RateLimitExceededException extends RuntimeException {
        public RateLimitExceededException(String msg) { super(msg); }
    }

    public static void main(String[] args) throws Exception {
        RateLimiter limiter = new RateLimiter(5, 100); // 5 concurrent, 100ms wait
        ExecutorService exec = Executors.newFixedThreadPool(20);

        for (int i = 0; i < 20; i++) {
            final int reqId = i;
            exec.submit(() -> {
                try {
                    String result = limiter.execute(() -> {
                        Thread.sleep(300); // simulate API call
                        return "Response-" + reqId;
                    });
                    System.out.println("Success: " + result);
                } catch (RateLimiter.RateLimitExceededException e) {
                    System.out.println("Rate limited: Request-" + reqId);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        exec.shutdown();
        exec.awaitTermination(10, TimeUnit.SECONDS);
        System.out.printf("Total: %d, Rejected: %d, Rate: %.1f%%%n",
            limiter.getTotalRequests(), limiter.getRejectedRequests(),
            limiter.getRejectionRate());
    }
}

Thread Lifecycle Flowchart

This flowchart shows the complete Java thread state machine β€” all states and every possible transition between them.

πŸ†• NEWnew Thread(runnable)
start()
β–Ά RUNNABLEthread.start() β€” eligible for CPU
CPU assigned
βš™οΈ RUNNINGJVM scheduler selects thread
yield() / preempted
πŸ”’ BLOCKEDWaiting for monitor lock
lock acquired
⏸️ WAITINGwait() / join() β€” indefinite
notify() / join ends
⏳ TIMED_WAITINGsleep(ms) / wait(ms) / join(ms)
timeout expires / notified
βœ… TERMINATEDrun() returned or threw exception

Code Execution Flow β€” from source to output

Java Multithreading Interview Questions β€” Beginner to Advanced

These questions are consistently asked in Java backend, system design, and senior developer interviews β€” covering both conceptual understanding and practical debugging skills.

Practice Questions β€” Test Your Multithreading Knowledge

Challenge yourself with these practice questions. Attempt each independently before reading the answer β€” concurrency bugs only reveal themselves when you reason carefully about interleaving.

1. What is the output of this code? Explain. class Counter { private int count = 0; public void increment() { count++; } public int getCount() { return count; } } // 10 threads each call increment() 1000 times. // What range of values can getCount() return?

Easy

2. Will this program terminate? If not, why? Fix it. class Test { static boolean flag = false; public static void main(String[] args) throws Exception { new Thread(() -> { while (!flag) {} System.out.println("Thread done"); }).start(); Thread.sleep(100); flag = true; System.out.println("Main done"); } }

Easy

3. Identify the deadlock scenario. What is the fix? Object lock1 = new Object(); Object lock2 = new Object(); Thread t1 = new Thread(() -> { synchronized(lock1) { Thread.sleep(50); synchronized(lock2) { System.out.println("T1 done"); } } }); Thread t2 = new Thread(() -> { synchronized(lock2) { Thread.sleep(50); synchronized(lock1) { System.out.println("T2 done"); } } });

Medium

4. Implement a thread-safe Singleton using double-checked locking. Why is volatile necessary?

Medium

5. Write a program that uses CountDownLatch to make the main thread wait until 5 worker threads complete their initialisation.

Medium

6. Convert this blocking Future-based code to a non-blocking CompletableFuture pipeline: String userId = "U1"; Future<User> userFuture = executor.submit(() -> fetchUser(userId)); User user = userFuture.get(); // blocks! Future<Order> orderFuture = executor.submit(() -> fetchOrders(user)); Order order = orderFuture.get(); // blocks again! System.out.println(order);

Medium

7. What is thread starvation? How can it happen even without deadlock? Write a scenario.

Hard

8. Design a thread-safe in-memory rate limiter that allows at most N requests per second globally across multiple threads.

Hard

Conclusion β€” Multithreading: The Engine Behind Every High-Performance Java Application

Multithreading is where Java's true power manifests β€” and where its most subtle bugs hide. Every high-performance Java application, from a REST API serving thousands of concurrent users to a trading platform processing millions of events per second, relies on carefully designed concurrency. The raw mechanisms (Thread, synchronized, wait/notify) give you full control; the high-level abstractions (ExecutorService, CompletableFuture, ConcurrentHashMap) give you safety and productivity.

The hallmark of a senior Java engineer in concurrent programming: they never create raw threads in production code, they understand memory visibility as deeply as mutual exclusion, they know when volatile is sufficient and when synchronized is necessary, they design for graceful shutdown from day one, they choose CompletableFuture pipelines over blocking Future.get(), and in Java 21 applications they reach for virtual threads first for I/O-bound concurrency.

ConceptPurposeModern Recommendation
Thread / RunnableDefine and start a concurrent taskRunnable via ExecutorService or virtual thread
synchronizedMutual exclusion β€” one thread at a timeUse for compound operations on shared mutable state
volatileVisibility β€” latest value seen by all threadsUse for simple flags; not for compound operations
wait() / notify()Thread coordination β€” signal when condition changesPrefer BlockingQueue or Condition for new code
ExecutorServiceManaged thread pool β€” reuse threads, lifecycle controlThreadPoolExecutor with bounded queue + rejection policy
Callable / FutureThread that returns a resultCompletableFuture.supplyAsync() is the modern choice
CompletableFutureNon-blocking async pipelines and compositionDefault choice for async Java 8+ code
ConcurrentHashMapThread-safe map with high read throughputAlways prefer over synchronizedMap() or Hashtable
BlockingQueueThread-safe producer-consumer bufferLinkedBlockingQueue or ArrayBlockingQueue
AtomicInteger / LongAdderLock-free single-variable thread safetyAtomicXxx for CAS; LongAdder for high-contention counters
ReentrantLock / SemaphoreAdvanced locking β€” timeout, fairness, multiple conditionsUse when synchronized cannot meet requirements
Virtual Threads (Java 21)Millions of lightweight threads for I/O-bound workReplace fixed thread pools for I/O-bound microservices

Your next step: Java Collections Framework β€” where concurrency meets data structures, and you'll learn how ConcurrentHashMap, CopyOnWriteArrayList, and the full java.util.concurrent collection suite are designed internally to achieve both correctness and maximum throughput under concurrent access. β˜•

Frequently Asked Questions β€” Java Multithreading