Java Multithreading - Synchronization, Locks and Thread Pools | Java Codeex

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 Rule: Always shut down an executor when work is complete.