Multithreading in Java

1. What is Multithreading?

A process is an independent program running in its own memory space. A thread is the smallest unit of execution within a process. Multiple threads share the same process memory, making communication between them fast and efficient.

Benefits of multithreading:

  • Concurrency — multiple tasks appear to run simultaneously, improving responsiveness.
  • CPU utilization — while one thread waits on I/O, another thread can use the CPU.
  • Simplified modeling — each independent task can live in its own thread (e.g., GUI + background download).

Note: On a single-core CPU, threads are interleaved (concurrency). On multi-core CPUs, threads may truly run in parallel (parallelism).

2. Thread Lifecycle

A Java thread transitions through the following states during its lifetime:

State Description
NEW Thread object created but start() not yet called.
RUNNABLE Thread is executing or ready to execute; waiting for CPU time.
BLOCKED Waiting to acquire a monitor lock held by another thread.
WAITING Waiting indefinitely for another thread (e.g., wait(), join()).
TIMED_WAITING Waiting for a specified period (e.g., sleep(ms), wait(ms)).
TERMINATED Thread has finished execution or was terminated by an exception.
// Check thread state at runtime
Thread t = new Thread(() -> System.out.println("running"));
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE or TERMINATED

3. Creating Threads — Extending Thread

The simplest way to create a thread is to subclass java.lang.Thread and override the run() method. Call start() to launch the new thread.

public class MyThread extends Thread {

    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(name + " — count: " + i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread("Thread-A");
        MyThread t2 = new MyThread("Thread-B");
        t1.start(); // starts a new thread
        t2.start(); // starts another new thread
    }
}
Thread-A — count: 1
Thread-B — count: 1
Thread-A — count: 2
Thread-B — count: 2
... (interleaved output)

Limitation: Java supports only single inheritance, so extending Thread prevents you from extending any other class. Prefer Runnable instead.

4. Creating Threads — Implementing Runnable

The preferred approach is to implement the Runnable interface and pass it to a Thread constructor. This separates the task logic from thread management and allows your class to extend another class.

public class PrintTask implements Runnable {

    private String message;

    public PrintTask(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 4; i++) {
            System.out.println(Thread.currentThread().getName()
                               + " says: " + message);
        }
    }

    public static void main(String[] args) {
        Runnable task1 = new PrintTask("Hello");
        Runnable task2 = new PrintTask("World");

        Thread t1 = new Thread(task1, "Worker-1");
        Thread t2 = new Thread(task2, "Worker-2");

        t1.start();
        t2.start();

        // Lambda shorthand (Java 8+)
        Thread t3 = new Thread(() -> System.out.println("Lambda thread!"), "Worker-3");
        t3.start();
    }
}
Worker-1 says: Hello
Worker-2 says: World
Worker-1 says: Hello
Worker-3 says: Lambda thread!
...

5. Thread.start() vs Thread.run()

This is a classic mistake. Understanding the difference is critical:

Method Behavior
start() Creates a new OS thread and schedules run() on it. Returns immediately.
run() Calls the run() method directly in the current thread. No new thread is created.
public class StartVsRun {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("Executing in: "
                + Thread.currentThread().getName());
        });

        // Correct — executes in a NEW thread
        t.start(); // prints "Executing in: Thread-0"

        // Wrong — executes in main thread
        // t.run(); // prints "Executing in: main"
    }
}

Rule: Always call start(). Calling run() directly gives you no concurrency benefit and can mislead you into thinking threads are working when they are not.

6. Thread.sleep()

Thread.sleep(milliseconds) pauses the current thread for at least the specified time. It does not release any locks the thread holds. It throws a checked InterruptedException which must be handled.

public class SleepDemo {
    public static void main(String[] args) {
        System.out.println("Task started at: " + java.time.LocalTime.now());

        try {
            Thread.sleep(2000); // pause for 2 seconds
        } catch (InterruptedException e) {
            // Restore interrupt status — best practice
            Thread.currentThread().interrupt();
            System.out.println("Thread was interrupted!");
        }

        System.out.println("Task resumed at: " + java.time.LocalTime.now());
    }
}
Task started at: 10:30:00.123
Task resumed at: 10:30:02.125

Important: Never swallow InterruptedException silently. Either rethrow it or restore the interrupt flag with Thread.currentThread().interrupt().

7. Thread Priority

Each Java thread has a priority from 1 (MIN) to 10 (MAX). The JVM scheduler uses priority as a hint — higher-priority threads are more likely to be scheduled first, but this is not guaranteed and varies by OS.

public class PriorityDemo {
    public static void main(String[] args) {
        Thread low  = new Thread(() -> System.out.println("Low priority"));
        Thread norm = new Thread(() -> System.out.println("Normal priority"));
        Thread high = new Thread(() -> System.out.println("High priority"));

        low.setPriority(Thread.MIN_PRIORITY);   // 1
        norm.setPriority(Thread.NORM_PRIORITY); // 5 (default)
        high.setPriority(Thread.MAX_PRIORITY);  // 10

        System.out.println("Low  priority value: " + low.getPriority());
        System.out.println("High priority value: " + high.getPriority());

        low.start();
        norm.start();
        high.start();
    }
}

Note: Do not rely on thread priorities for correctness. Use them only as performance tuning hints. Program logic must never depend on scheduling order.

8. Daemon Threads

A daemon thread is a background service thread (e.g., garbage collector). The JVM will exit when all non-daemon (user) threads finish, even if daemon threads are still running. Call setDaemon(true) before start().

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread daemon = new Thread(() -> {
            while (true) {
                System.out.println("Daemon thread running...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });

        daemon.setDaemon(true);  // must be called before start()
        daemon.start();

        System.out.println("Is daemon? " + daemon.isDaemon()); // true

        Thread.sleep(1500); // main thread sleeps 1.5s
        System.out.println("Main thread finishing — JVM will exit");
        // JVM exits here; daemon thread is killed automatically
    }
}
Is daemon? true
Daemon thread running...
Daemon thread running...
Daemon thread running...
Main thread finishing — JVM will exit

9. Synchronization

When multiple threads access shared mutable data, a race condition can corrupt state. Use synchronized to allow only one thread at a time.

Race Condition (Problem)

public class Counter {
    private int count = 0;

    // NOT thread-safe — race condition!
    public void increment() {
        count++; // read-modify-write: three separate operations
    }

    public int getCount() { return count; }

    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();
        Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.increment(); });
        Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) c.increment(); });
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("Expected 2000, got: " + c.getCount()); // often less!
    }
}

Synchronized Method (Fix)

public class SynchronizedCounter {
    private int count = 0;

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

    public synchronized int getCount() { return count; }
}

Synchronized Block (Fine-grained control)

public class BankAccount {
    private double balance;
    private final Object lock = new Object();

    public void deposit(double amount) {
        synchronized (lock) {  // lock on specific object
            balance += amount;
            System.out.println("Deposited: " + amount
                               + " | Balance: " + balance);
        }
    }

    public void withdraw(double amount) {
        synchronized (lock) {
            if (balance >= amount) {
                balance -= amount;
                System.out.println("Withdrew: " + amount
                                   + " | Balance: " + balance);
            }
        }
    }
}

10. Thread Safety

Beyond synchronized, Java provides several mechanisms for safe concurrent access:

volatile Keyword

Marks a variable so reads/writes go directly to main memory, preventing thread-local caching. Use when one thread writes and others only read.

public class VolatileDemo {
    // Without volatile, the loop in another thread might never see the update
    private volatile boolean running = true;

    public void stop() {
        running = false;
    }

    public void run() {
        while (running) {
            // do work
        }
        System.out.println("Thread stopped cleanly.");
    }
}

Atomic Operations

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // atomic read-modify-write
    }

    public int getCount() {
        return count.get();
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicDemo demo = new AtomicDemo();
        Thread t1 = new Thread(() -> { for(int i=0;i<1000;i++) demo.increment(); });
        Thread t2 = new Thread(() -> { for(int i=0;i<1000;i++) demo.increment(); });
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("Count (always 2000): " + demo.getCount());
    }
}

11. Locks (ReentrantLock)

ReentrantLock from java.util.concurrent.locks provides explicit, flexible locking with features like tryLock() and timed acquisition that synchronized cannot offer.

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

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

    public void tryIncrement() {
        if (lock.tryLock()) {  // acquire only if immediately available
            try {
                count++;
                System.out.println("Lock acquired, count = " + count);
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Could not acquire lock, skipping.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo demo = new LockDemo();
        Thread t1 = new Thread(() -> { for(int i=0;i<500;i++) demo.increment(); });
        Thread t2 = new Thread(() -> { for(int i=0;i<500;i++) demo.increment(); });
        t1.start(); t2.start();
        t1.join();  t2.join();
        System.out.println("Final count: " + demo.count); // always 1000
    }
}

12. Deadlock

A deadlock occurs when two or more threads are each waiting for a lock held by the other, forming a circular dependency from which none can proceed.

Deadlock Example

public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("T1 holds A, waiting for B...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {  // DEADLOCK — t2 holds B
                    System.out.println("T1 acquired B");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("T2 holds B, waiting for A...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockA) {  // DEADLOCK — t1 holds A
                    System.out.println("T2 acquired A");
                }
            }
        });

        t1.start();
        t2.start();
        // Both threads hang forever!
    }
}

Prevention Strategies

  • Lock ordering: Always acquire locks in the same order across all threads.
  • tryLock with timeout: Use lock.tryLock(timeout, unit) and back off if not acquired.
  • Minimize lock scope: Hold locks for the shortest time possible.
  • Use higher-level concurrency utilities from java.util.concurrent instead of manual locking.

13. Thread Pools and Executor Framework

Creating a new thread for every task is expensive. A thread pool reuses a fixed set of threads. The Executor framework (Java 5+) manages pools for you.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // Pool of 4 worker threads
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId
                    + " running in " + Thread.currentThread().getName());
                try {
                    Thread.sleep(500); // simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown(); // no new tasks accepted
        System.out.println("All tasks submitted.");
        // executor.awaitTermination(10, TimeUnit.SECONDS) to wait
    }
}
Task 1 running in pool-1-thread-1
Task 2 running in pool-1-thread-2
Task 3 running in pool-1-thread-3
Task 4 running in pool-1-thread-4
Task 5 running in pool-1-thread-1
...
Factory Method Behavior
newFixedThreadPool(n) Fixed pool of n threads; queues extra tasks.
newCachedThreadPool() Creates threads as needed; reuses idle ones. Unbounded.
newSingleThreadExecutor() Single worker thread; tasks execute sequentially.
newScheduledThreadPool(n) Supports delayed and periodic task scheduling.

14. Callable and Future

Runnable cannot return a value or throw checked exceptions. Use Callable<V> when you need a result. Future<V> holds the eventual return value and lets you check status or cancel.

import java.util.concurrent.*;

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

        // Callable returns a result
        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            Thread.sleep(1000); // simulate computation
            return sum;
        };

        Callable<String> greetTask = () -> {
            Thread.sleep(500);
            return "Hello from Callable!";
        };

        Future<Integer> sumFuture   = executor.submit(sumTask);
        Future<String>  greetFuture = executor.submit(greetTask);

        System.out.println("Tasks submitted, doing other work...");

        // get() blocks until result is ready
        String greeting = greetFuture.get();           // waits ~500ms
        Integer sum     = sumFuture.get(3, TimeUnit.SECONDS); // timeout

        System.out.println("Greeting: " + greeting);
        System.out.println("Sum 1-100: " + sum);

        executor.shutdown();
    }
}
Tasks submitted, doing other work...
Greeting: Hello from Callable!
Sum 1-100: 5050

15. Virtual Threads — Java 21

Introduced as a stable feature in Java 21 (Project Loom), virtual threads are lightweight threads managed by the JVM — not the OS. You can create millions of them without exhausting system resources, making them ideal for high-throughput I/O-bound applications.

public class VirtualThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // Create a single virtual thread
        Thread vt = Thread.ofVirtual()
                          .name("my-virtual-thread")
                          .start(() -> System.out.println(
                              "Running in: " + Thread.currentThread()));

        vt.join();

        // Launch 100,000 virtual threads — trivial resource usage
        var threads = new java.util.ArrayList<Thread>();
        for (int i = 0; i < 100_000; i++) {
            final int id = i;
            threads.add(Thread.ofVirtual().start(() -> {
                // simulate I/O wait
                try { Thread.sleep(1000); }
                catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }));
        }
        for (Thread t : threads) t.join();
        System.out.println("All 100,000 virtual threads completed!");

        // With ExecutorService (recommended)
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10; i++) {
                final int taskId = i;
                executor.submit(() ->
                    System.out.println("VT Task " + taskId
                        + " in " + Thread.currentThread()));
            }
        } // auto-shutdown
    }
}
Running in: VirtualThread[#21,my-virtual-thread]/runnable@ForkJoinPool...
All 100,000 virtual threads completed!
VT Task 0 in VirtualThread[#22]/runnable@...
...

Java 21+ required. Virtual threads are best for I/O-bound work (HTTP calls, DB queries). CPU-bound tasks still benefit from traditional platform threads with proper pool sizing.

Continue learning