Chapter 17: Multithreading
In 2026, almost every serious application (web servers, Android apps, big data processing, games, banking systems) uses multithreading to do many things at the same time — download a file while showing progress bar, handle 1000 user requests simultaneously, process data in background without freezing the UI, etc.
We’ll go super slowly, like we’re sitting together in a quiet Bandra café with the sea breeze — I’ll explain every concept step by step with real-life analogies, complete runnable programs, detailed breakdowns, tables, common mistakes with fixes, and tons of examples you can copy-paste and run right now.
Let’s dive in!
1. What is Multithreading? (The Big Idea)
Multithreading means multiple threads (lightweight subprocesses) running concurrently inside the same process (same program).
- Process = heavy, has its own memory
- Thread = lightweight, shares the same memory with other threads in the process
Real-life analogy: You are cooking dinner (single-threaded):
- Boil water → wait → chop vegetables → wait → fry → wait Everything is sequential — slow!
Multithreading:
- One thread boils water
- Another chops vegetables
- Another fries onions All happening at the same time → dinner ready much faster!
Benefits:
- Better performance (use multiple CPU cores)
- Responsiveness (UI doesn’t freeze while doing heavy work)
- Parallel processing (download + process + update UI)
Risks:
- Race conditions
- Deadlocks
- Thread interference
2. Two Ways to Create Threads in Java
Way 1: Extending the Thread class
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// Step 1: Create a class that extends Thread public class MyThread extends Thread { @Override public void run() { // This is the entry point — like main() for (int i = 1; i <= 5; i++) { System.out.println("Thread " + Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); // Pause 500ms } catch (InterruptedException e) { e.printStackTrace(); } } } } // Step 2: Create and start the thread public class ThreadDemo1 { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); t1.setName("Worker-1"); t2.setName("Worker-2"); t1.start(); // Starts the thread → calls run() t2.start(); System.out.println("Main thread finished!"); } } |
Output (order may vary — threads run concurrently):
|
0 1 2 3 4 5 6 7 8 9 10 11 |
Main thread finished! Thread Worker-1 : 1 Thread Worker-2 : 1 Thread Worker-1 : 2 Thread Worker-2 : 2 ... |
Way 2: Implementing the Runnable interface (Recommended)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// Step 1: Implement Runnable public class MyRunnable implements Runnable { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println("Runnable " + Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } // Step 2: Create Thread object and pass Runnable public class ThreadDemo2 { public static void main(String[] args) { Runnable r1 = new MyRunnable(); Runnable r2 = new MyRunnable(); Thread t1 = new Thread(r1, "Worker-A"); Thread t2 = new Thread(r2, "Worker-B"); t1.start(); t2.start(); System.out.println("Main thread finished!"); } } |
Why prefer Runnable?
- You can extend another class (Java doesn’t allow multiple inheritance)
- Better separation of task (Runnable) and thread (Thread)
3. Thread Lifecycle (The 6 States)
| State | Description | How to reach / leave |
|---|---|---|
| NEW | Thread created but not started (new Thread()) | Call start() → RUNNABLE |
| RUNNABLE | Ready to run or actually running | Gets CPU time or yields → stays RUNNABLE |
| BLOCKED | Waiting for a monitor lock (synchronized block) | Acquires lock → RUNNABLE |
| WAITING | Waiting indefinitely (wait(), join(), LockSupport.park()) | notify(), notifyAll(), interrupt(), timeout → RUNNABLE |
| TIMED_WAITING | Waiting with timeout (sleep(), wait(timeout), join(timeout)) | Time up, notify, interrupt → RUNNABLE |
| TERMINATED | Thread finished execution or exception thrown | End of run() method |
Visual:
|
0 1 2 3 4 5 6 |
NEW → start() → RUNNABLE ↔ BLOCKED / WAITING / TIMED_WAITING → TERMINATED |
4. Synchronization – Preventing Race Conditions
When multiple threads access shared data, you get race conditions → wrong results.
Example without synchronization (wrong balance!):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class Counter { private int count = 0; public void increment() { count++; // NOT atomic! (read → increment → write) } public int getCount() { return count; } } public class RaceConditionDemo { public static void main(String[] args) throws InterruptedException { Counter c = new Counter(); Runnable task = () -> { for (int i = 0; i < 10000; i++) { c.increment(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + c.getCount()); // Should be 20000, but usually less! } } |
Fix: Synchronization
- Synchronized method
|
0 1 2 3 4 5 6 7 8 |
public synchronized void increment() { count++; } |
- Synchronized block (better – smaller scope)
|
0 1 2 3 4 5 6 7 8 9 10 |
public void increment() { synchronized (this) { // or synchronized(Counter.class) for static count++; } } |
- Using volatile (only visibility, not atomicity)
|
0 1 2 3 4 5 6 |
private volatile int count = 0; // Ensures latest value is seen by all threads |
5. Locks (More Flexible Synchronization – Java 5+)
ReentrantLock gives more control than synchronized.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import java.util.concurrent.locks.ReentrantLock; public class CounterWithLock { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // Acquire lock try { count++; } finally { lock.unlock(); // Always release! } } } |
6. Thread Pools & ExecutorService (Modern & Recommended Way)
Manually creating 1000 threads is bad — too much overhead.
ExecutorService + ThreadPool reuses threads.
Example: Using Fixed Thread Pool
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { public static void main(String[] args) { // Create a pool of 3 threads ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 1; i <= 10; i++) { int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); // No new tasks accepted // executor.awaitTermination(10, TimeUnit.SECONDS); // Optional: wait for finish System.out.println("All tasks submitted!"); } } |
Common Executors:
- Executors.newFixedThreadPool(n) → fixed number of threads
- Executors.newCachedThreadPool() → creates threads as needed, reuses idle ones
- Executors.newSingleThreadExecutor() → only one thread (like sequential)
- Executors.newScheduledThreadPool() → for scheduled tasks
Quick Recap Table (Your Cheat Sheet)
| Concept | Key Points / Best Practice | Example |
|---|---|---|
| Create Thread | Prefer implements Runnable | new Thread(runnable).start(); |
| Thread Lifecycle | NEW → RUNNABLE → BLOCKED/WAITING → TERMINATED | start(), sleep(), join() |
| Synchronization | Use synchronized or Lock on shared data | synchronized (obj) { … } |
| volatile | Ensures visibility, not atomicity | private volatile boolean flag; |
| Thread Pool | Use ExecutorService – never create 1000s of threads | Executors.newFixedThreadPool(10) |
| join() | Wait for thread to finish | t1.join(); |
| sleep() vs wait() | sleep() → static, hold lock; wait() → release lock | — |
Common Mistakes & Fixes
| Mistake | Problem | Fix |
|---|---|---|
| Call run() instead of start() | Runs in main thread – no multithreading | Always call start() |
| No finally { lock.unlock(); } | Deadlock – lock never released | Always release lock in finally |
| Accessing shared data without sync | Race condition – wrong results | Use synchronized or Lock |
| Not shutting down ExecutorService | Program hangs | Call executor.shutdown() |
| Catching InterruptedException silently | Thread may not stop properly | Restore interrupt: Thread.currentThread().interrupt(); |
Homework for You (Practice to Master!)
- Basic: Create 5 threads using Runnable — each prints numbers 1 to 10 with 200ms delay.
- Medium: Create a shared Counter class. Increment it 10000 times from 3 threads. Fix race condition using synchronized.
- Advanced: Use ExecutorService with 4 threads to process 20 tasks (print task ID + thread name).
- Fun: Create two threads: one prints “Tic” every 1s, another prints “Tac” every 1s — they should alternate.
- Challenge: Implement a thread-safe Singleton class using double-checked locking.
You’re doing amazing! Multithreading is the heart of modern Java applications — now you can write fast, concurrent, real-world code.
