Core Java · Lesson 10
Multithreading
Concurrency: Running Code in Parallel
Multithreading allows multiple tasks to run concurrently (seemingly at the same time) within a single program. A thread is a lightweight process. On multi-core processors, threads truly run in parallel; on single cores, the OS switches between threads rapidly. Multithreading is essential for responsive applications, server handling multiple clients, and performing time-consuming tasks without blocking the UI.
Creating Threads: Two Approaches
Method 1: Extend the Thread class and override the run() method. Method 2: Implement the Runnable interface and pass it to a Thread constructor. The Runnable approach is preferred because Java doesn't support multiple inheritance — implementing Runnable leaves room for extending another class.
// Method 1: Extend 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(1000); // Pause for 1 second
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
System.out.println(name + " finished");
}
}
// Method 2: Implement Runnable (preferred)
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(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(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
System.out.println(name + " finished");
}
}
public class ThreadDemo {
public static void main(String[] args) {
// Method 1: Using Thread subclass
MyThread thread1 = new MyThread("Thread-1");
thread1.start(); // Call start(), not run()!
// Method 2: Using Runnable
Thread thread2 = new Thread(new MyRunnable("Thread-2"));
thread2.start();
// Both threads run concurrently
System.out.println("Main thread continues");
}
}
Thread States and Lifecycle
Threads go through several states: New (created, not started), Runnable (running or waiting to run), Blocked (waiting for a resource), Waiting (waiting for another thread), and Terminated (finished). Understanding these states helps you debug concurrent issues and control thread behavior.
public class ThreadLifecycle {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker thread started");
try {
Thread.sleep(2000); // Simulating work
} catch (InterruptedException e) {
System.out.println("Worker interrupted");
}
System.out.println("Worker thread finished");
});
System.out.println("State before start: " + worker.getState()); // NEW
worker.start();
System.out.println("State after start: " + worker.getState()); // RUNNABLE
Thread.sleep(1000);
System.out.println("State during sleep: " + worker.getState()); // TIMED_WAITING
worker.join(); // Main thread waits for worker to finish
System.out.println("State after finish: " + worker.getState()); // TERMINATED
}
}
Thread Synchronization: Managing Shared Data
When multiple threads access the same data, race conditions occur — unpredictable results from interleaved access. Use the synchronized keyword to ensure only one thread accesses a method or block at a time. This protects shared state but can impact performance.
public class Counter {
private int count = 0;
// Synchronized method: only one thread at a time
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
// Alternative: synchronized block (more fine-grained)
public void incrementWithBlock() {
synchronized (this) {
count++;
}
}
}
public class SynchronizationDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create 10 threads, each incrementing 1000 times
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// Wait for all threads to finish
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final count: " + counter.getCount()); // 10000
}
}
Thread Communication: wait() and notify()
Threads often need to coordinate — one thread waits while another prepares data. Use wait() to pause a thread until notified, and notify() or notifyAll() to wake waiting threads. This avoids busy-waiting (constantly checking) and improves efficiency.
public class Producer implements Runnable {
private DataBuffer buffer;
public Producer(DataBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
buffer.produce("Item-" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public class Consumer implements Runnable {
private DataBuffer buffer;
public Consumer(DataBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
buffer.consume();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public class DataBuffer {
private String data = null;
private boolean hasData = false;
public synchronized void produce(String item) {
while (hasData) {
try {
wait(); // Wait until consumer reads
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
this.data = item;
this.hasData = true;
System.out.println("Produced: " + item);
notify(); // Wake consumer
}
public synchronized String consume() {
while (!hasData) {
try {
wait(); // Wait until producer writes
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
String item = this.data;
this.hasData = false;
System.out.println("Consumed: " + item);
notify(); // Wake producer
return item;
}
}
Common Mistake: Calling
run()instead ofstart().thread.run()executes immediately in the current thread (not concurrent). Always callthread.start(), which spawns a new thread. Also, modifying shared data without synchronization causes unpredictable bugs — if multiple threads touch the same object, protect it withsynchronized.
Best Practices for Multithreading
- Prefer
Runnableover extendingThread: leaves room for inheritance - Use
Thread.sleep()for delays, not busy loops: saves CPU - Synchronize access to shared mutable state: prevents race conditions
- Use
synchronizedblocks (not methods) for better performance - Use
join()to wait for threads to finish before continuing - Handle
InterruptedExceptiongracefully: stop threads cleanly - Avoid deadlocks: acquire locks in consistent order across threads
- Consider higher-level concurrency utilities (ExecutorService, Locks) for complex scenarios
Real-World Example: Web Server Handling Multiple Clients
A Java web server creates a new thread for each client connection. The main thread runs a loop accepting connections, spawning a ClientHandler thread for each. Multiple threads run concurrently, handling requests independently — if one client is slow, others aren't blocked. Shared resources like a thread-safe connection pool use synchronized access. When a request requires data from a database, threads wait for the response without blocking others. This architecture allows a single server to serve hundreds or thousands of clients efficiently.
Quiz Check