What Is It?
What Is Multithreading?
A thread is the smallest unit of execution within a process. A Java program starts with one thread -- the main thread. Multithreading means running multiple threads concurrently within the same program, allowing different parts of your code to execute simultaneously.
public class Main {
public static void main(String[] args) {
System.out.println("Current thread: " + Thread.currentThread().getName());
// Output: Current thread: main
}
}Thread vs Process
A process is an independent program running in its own memory space. A thread is a lightweight unit of execution within a process. Key differences:
- Memory: Processes have separate memory spaces. Threads within the same process share the same heap memory but have their own stack.
- Creation cost: Creating a thread is cheaper than creating a process.
- Communication: Threads can communicate directly through shared memory. Processes need inter-process communication (IPC) mechanisms.
- Isolation: A crash in one process does not affect others. A crash in one thread can bring down the entire process.
When Arjun's web server handles 100 users simultaneously, it does not create 100 separate processes. It creates 100 threads within one process, sharing the same application code and data.
Why Does It Matter?
Why Is Multithreading Important?
1. Better Utilization of CPU Resources
Modern CPUs have multiple cores. A single-threaded program uses only one core while the rest sit idle. Multithreading allows your program to use all available cores. If Priya's data processing application runs 4 threads on a 4-core CPU, it can process data up to 4 times faster than a single-threaded version.
2. Responsive User Interfaces
In a GUI application, if a heavy computation runs on the main thread, the interface freezes. By running the computation on a separate thread, the UI remains responsive. This is why Android mandates that network calls happen on background threads.
3. Server-Side Scalability
Web servers use threads to handle multiple client requests simultaneously. When Vikram's Spring Boot application receives 1000 requests per second, each request is handled by a separate thread from a thread pool. Without multithreading, the server would process requests one at a time.
4. Real-World Applications
Multithreading is everywhere: web servers (Tomcat uses thread pools), databases (concurrent query processing), games (rendering + physics + AI on separate threads), download managers (parallel file downloads), and IDEs (code compilation while you type).
5. Interview and Placement Significance
Multithreading is one of the most asked topics in Java interviews at companies like Amazon, Flipkart, Goldman Sachs, and Morgan Stanley. Questions on thread lifecycle, synchronization, deadlock, and the volatile keyword appear regularly.
Detailed Explanation
Detailed Explanation
1. Creating Threads: Extending the Thread Class
The simplest way to create a thread is to extend the Thread class and override its run() method:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + ": count " + i);
try {
Thread.sleep(500); // pause for 500ms
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("Thread-A");
t2.setName("Thread-B");
t1.start(); // creates a new thread and calls run()
t2.start();
System.out.println("Main thread continues...");
}
}Critical point: You must call start(), not run(). Calling run() directly executes the method on the current thread (no new thread is created). start() creates a new thread and then invokes run() on that new thread.
2. Creating Threads: Implementing Runnable
The preferred approach is to implement the Runnable interface:
class MyTask implements Runnable {
private String taskName;
public MyTask(String name) {
this.taskName = name;
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(taskName + ": step " + i);
}
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask("Download"));
Thread t2 = new Thread(new MyTask("Upload"));
t1.start();
t2.start();
}
}Why prefer Runnable over extending Thread? (1) Java allows only single inheritance -- if you extend Thread, you cannot extend another class. (2) Runnable separates the task from the thread mechanism, following better design. (3) Runnable tasks can be submitted to thread pools (Executor framework).
3. Thread Lifecycle
A thread goes through five states during its lifetime:
NEW -> RUNNABLE -> RUNNING -> TERMINATED
| ^
v |
BLOCKED/WAITING- NEW: Thread object created but
start()not yet called. - RUNNABLE: After
start()is called, the thread is ready to run but waiting for CPU time. - RUNNING: The thread scheduler has assigned CPU time and the thread is executing.
- BLOCKED/WAITING: The thread is alive but not eligible to run. This happens when waiting for a lock (BLOCKED), calling
wait()(WAITING), or callingsleep()/join()with timeout (TIMED_WAITING). - TERMINATED: The
run()method has completed (or an unhandled exception occurred). The thread cannot be restarted.
4. Important Thread Methods
Thread t = new Thread(() -> {
System.out.println("Running: " + Thread.currentThread().getName());
});
// Before start
t.setName("Worker-1"); // Set thread name
t.setPriority(Thread.MAX_PRIORITY); // 1 to 10 (default 5)
t.setDaemon(true); // Daemon thread (ends when main ends)
t.start(); // Start the thread
// Thread info
System.out.println(t.getName()); // Worker-1
System.out.println(t.isAlive()); // true if not terminated
System.out.println(t.getState()); // NEW, RUNNABLE, etc.
// Static methods (affect current thread)
Thread.sleep(1000); // Pause current thread for 1 second
Thread.currentThread(); // Get reference to current thread
Thread.yield(); // Hint to scheduler to give other threads a turn5. join() -- Waiting for a Thread to Finish
Thread t = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
System.out.println("Main waiting...");
t.join(); // Main thread blocks here until t finishes
System.out.println("Main continues after thread finished");join() makes the calling thread wait until the target thread terminates. This is essential when you need the result of a thread before proceeding. Without join(), the main thread would continue immediately without waiting.
6. Synchronization
When multiple threads access shared data, race conditions can occur. Synchronization ensures that only one thread accesses a critical section at a time:
class BankAccount {
private int balance = 1000;
// Synchronized method -- only one thread can execute this at a time
public synchronized void withdraw(int amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName()
+ " withdrawing " + amount);
balance -= amount;
System.out.println("Remaining balance: " + balance);
} else {
System.out.println(Thread.currentThread().getName()
+ ": insufficient funds");
}
}
public synchronized int getBalance() {
return balance;
}
}Without synchronized, two threads could both check balance >= amount simultaneously, both see sufficient funds, and both withdraw -- resulting in a negative balance. The synchronized keyword acquires a lock on the object, allowing only one thread to execute any synchronized method on that object at a time.
Synchronized Blocks
Instead of synchronizing the entire method, you can synchronize only the critical section:
public void withdraw(int amount) {
// Non-critical code can run concurrently
System.out.println("Processing request...");
synchronized (this) {
// Only this block is locked
if (balance >= amount) {
balance -= amount;
}
}
}Synchronized blocks are preferred when only a small part of the method needs protection. They reduce the time a thread holds a lock, improving concurrency.
7. The volatile Keyword
volatile ensures that reads and writes to a variable go directly to main memory, not a thread-local cache:
class StopFlag {
private volatile boolean running = true;
public void stop() {
running = false; // Immediately visible to all threads
}
public void run() {
while (running) {
// Do work
}
System.out.println("Stopped.");
}
}Without volatile, a thread might cache the value of running and never see the update from another thread, causing an infinite loop. volatile guarantees visibility but does NOT guarantee atomicity -- it is not a replacement for synchronized when you need compound operations (check-then-act).
8. Deadlock
A deadlock occurs when two or more threads are blocked forever, each waiting for a lock that the other holds:
Object lockA = new Object();
Object lockB = new Object();
// Thread 1: locks A, then tries to lock B
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("T1: holding lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("T1: holding lockA and lockB");
}
}
});
// Thread 2: locks B, then tries to lock A
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("T2: holding lockB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("T2: holding lockB and lockA");
}
}
});
t1.start();
t2.start();
// DEADLOCK: T1 holds A, waits for B. T2 holds B, waits for A.How to avoid deadlocks: (1) Always acquire locks in the same order. (2) Use timeouts with tryLock() from java.util.concurrent.locks. (3) Minimize the scope and duration of locks. (4) Avoid nested locks when possible.
9. Inter-Thread Communication: wait(), notify(), notifyAll()
Threads can communicate using wait(), notify(), and notifyAll(). These methods must be called from within a synchronized block:
class SharedBuffer {
private int data;
private boolean hasData = false;
public synchronized void produce(int value) throws InterruptedException {
while (hasData) {
wait(); // Release lock and wait until consumed
}
data = value;
hasData = true;
System.out.println("Produced: " + value);
notify(); // Wake up waiting consumer
}
public synchronized int consume() throws InterruptedException {
while (!hasData) {
wait(); // Release lock and wait until produced
}
hasData = false;
System.out.println("Consumed: " + data);
notify(); // Wake up waiting producer
return data;
}
}wait() releases the lock and puts the thread to sleep until another thread calls notify() on the same object. notify() wakes up one waiting thread. notifyAll() wakes up all waiting threads. Always use while (not if) before wait() to guard against spurious wakeups.
10. Executor Framework
Creating threads manually is inefficient for large-scale applications. The Executor framework provides thread pools:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
public static void main(String[] args) {
// Create a pool of 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit 5 tasks to the pool
for (int i = 1; i <= 5; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId
+ " running on " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) {}
});
}
executor.shutdown(); // No new tasks, finish existing ones
System.out.println("All tasks submitted.");
}
}A thread pool reuses a fixed number of threads to execute multiple tasks. When a thread finishes one task, it picks up the next from the queue. This avoids the overhead of creating and destroying threads repeatedly. shutdown() initiates an orderly shutdown -- no new tasks are accepted, but existing tasks complete.
Code Examples
No code examples available yet.
Common Mistakes
No common mistakes listed for this topic.
Summary
Summary coming soon.