Solution and explanation
What it is
Multithreading in Java is a feature that lets a program run multiple threads at the same time, so tasks execute concurrently (and truly in parallel on multi-core CPUs) and the CPU is used more efficiently. A thread is a lightweight, independent unit of execution inside a process.
- Threads allow parallel execution of tasks.
- A process can have multiple threads.
- Each thread runs independently but shares the same memory.
Example: imagine a restaurant kitchen. Several chefs (threads) prepare different dishes at the same time. Service is faster and every stove (CPU core) is in use.
Main program (restaurant manager)
|
starts multiple threads (chefs)
┌──────────┬──────────┬──────────┐
Thread 1 Thread 2 Thread 3 Thread 4
(Chef A) (Chef B) (Chef C) (Chef D)
run() run() run() run()
└──────────┴──── concurrent execution ┘
Way 1: extending the Thread class
Create a class that extends Thread and override run() with the task. Create objects and call start(), which creates a new thread and calls run() on it.
class CookingTask extends Thread {
private final String task;
CookingTask(String task) { this.task = task; }
@Override
public void run() {
System.out.println(task + " is being prepared by " + Thread.currentThread().getName());
}
}
public class Restaurant {
public static void main(String[] args) {
Thread t1 = new CookingTask("Pasta");
Thread t2 = new CookingTask("Salad");
Thread t3 = new CookingTask("Dessert");
Thread t4 = new CookingTask("Rice");
t1.start(); t2.start(); t3.start(); t4.start();
}
}
Output (one possible order)
Rice is being prepared by Thread-3
Pasta is being prepared by Thread-0
Dessert is being prepared by Thread-2
Salad is being prepared by Thread-1
Explanation:
- We created four threads (t1–t4) from the CookingTask class; each represents a dish.
- start() creates a new thread with its own call stack and internally invokes run(), so the threads run concurrently with main and with each other.
- Execution order is not fixed: after start(), the JVM and operating system decide when each thread runs, so output may differ on every run.
Way 2: implementing Runnable
Create a class that implements java.lang.Runnable, define run(), pass an instance to a Thread constructor and call start().
class CookingJob implements Runnable {
private final String task;
CookingJob(String task) { this.task = task; }
@Override
public void run() {
System.out.println(task + " is being prepared by " + Thread.currentThread().getName());
}
}
public class RestaurantRunnable {
public static void main(String[] args) {
Thread t1 = new Thread(new CookingJob("Soup"));
Thread t2 = new Thread(new CookingJob("Pizza"));
Thread t3 = new Thread(new CookingJob("Burger"));
t1.start(); t2.start(); t3.start();
}
}
Output (one possible order)
Burger is being prepared by Thread-2
Pizza is being prepared by Thread-1
Soup is being prepared by Thread-0
With Java 8 lambdas: new Thread(() -> System.out.println("Tea ready")).start();
Which way to use
- extends Thread: only if your class does not extend any other class.
- implements Runnable: preferred – your class can still extend another class (Java has no multiple inheritance), and the task is separated from the thread that runs it.
- In real applications use an ExecutorService (thread pool) rather than creating threads by hand.
Waiting for threads to finish
t1.join(); // main waits until t1 completes
Advantages
- Improved performance: multiple tasks run simultaneously.
- Efficient CPU utilisation: cores stay busy.
- Responsiveness: GUIs and servers stay responsive while background work runs.
- Resource sharing: threads share memory, avoiding duplication.
- Better user experience: downloads, animations and real-time updates run smoothly.
Challenges
- Race conditions when threads modify shared data (use synchronization or atomic classes).
- Deadlocks, starvation, harder debugging and testing.
- Too many threads add context-switching overhead.
Note: a web server like Tomcat is multithreaded – each incoming HTTP request is handled by a thread from a pool.