1. Introduction to Multithreading
What is Multithreading?
Multithreading is a programming concept that allows multiple threads to execute concurrently within a single program. Each thread represents an independent path of execution.
Why Use Multithreading?
- Better CPU utilization
- Improved performance for I/O bound tasks
- Responsive applications (UI doesn't freeze)
- Parallel processing capabilities
- Better resource utilization
Key Concepts
- Thread: Lightweight sub-process, smallest unit of execution
- Process: Independent program with its own memory space
- Concurrency: Multiple tasks making progress simultaneously
- Parallelism: Multiple tasks executing at exactly the same time
2. Thread Lifecycle
public class ThreadLifecycle {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Thread Lifecycle Demo ===");
Thread thread = new Thread(() -> {
try {
System.out.println("Thread is RUNNING");
Thread.sleep(1000); // TIMED_WAITING
System.out.println("Thread completed execution");
} catch (InterruptedException e) {
System.out.println("Thread was INTERRUPTED");
}
});
System.out.println("Thread state: " + thread.getState()); // NEW
thread.start();
System.out.println("Thread state after start: " + thread.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println("Thread state during sleep: " + thread.getState()); // TIMED_WAITING
thread.join(); // Wait for thread to complete
System.out.println("Thread state after completion: " + thread.getState()); // TERMINATED
}
}
Thread States:
- NEW - Created but not started
- RUNNABLE - Ready to run or running
- BLOCKED - Waiting for monitor lock
- WAITING - Waiting indefinitely for another thread
- TIMED_WAITING - Waiting for specified time
- TERMINATED - Execution completed
3. Creating Threads in Java
Method 1: Extending Thread Class
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
System.out.println(threadName + " is running");
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " - Count: " + i);
try {
Thread.sleep(500); // Simulate some work
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted");
}
}
System.out.println(threadName + " finished");
}
}
public class ExtendThreadExample {
public static void main(String[] args) {
System.out.println("Main thread started");
MyThread thread1 = new MyThread("Thread-1");
MyThread thread2 = new MyThread("Thread-2");
thread1.start(); // Start thread1
thread2.start(); // Start thread2
try {
thread1.join(); // Wait for thread1 to complete
thread2.join(); // Wait for thread2 to complete
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished");
}
}
Method 2: Implementing Runnable Interface
class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String name) {
this.threadName = name;
}
@Override
public void run() {
System.out.println(threadName + " is running");
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + " - Count: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted");
}
}
System.out.println(threadName + " finished");
}
}
public class RunnableExample {
public static void main(String[] args) {
System.out.println("Main thread started");
Thread thread1 = new Thread(new MyRunnable("Runnable-1"));
Thread thread2 = new Thread(new MyRunnable("Runnable-2"));
Thread thread3 = new Thread(() -> {
System.out.println("Lambda thread running");
for (int i = 1; i <= 3; i++) {
System.out.println("Lambda - Count: " + i);
}
});
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished");
}
}
Method 3: Using Lambda Expressions (Java 8+)
public class LambdaThreadExample {
public static void main(String[] args) {
System.out.println("=== Lambda Threads ===");
// Simple lambda thread
Thread lambdaThread = new Thread(() -> {
System.out.println("Lambda thread executed by: " + Thread.currentThread().getName());
});
// Multiple lambda threads
for (int i = 1; i <= 5; i++) {
final int threadId = i;
Thread thread = new Thread(() -> {
System.out.println("Thread " + threadId + " executed by: " +
Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
lambdaThread.start();
}
}
4. Thread Methods and Control
public class ThreadMethodsDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Thread Methods Demo ===");
Thread worker1 = new Thread(() -> {
System.out.println("Worker 1 started");
try {
for (int i = 1; i <= 10; i++) {
System.out.println("Worker 1 - " + i);
Thread.sleep(200);
}
} catch (InterruptedException e) {
System.out.println("Worker 1 interrupted!");
}
});
Thread worker2 = new Thread(() -> {
System.out.println("Worker 2 started");
for (int i = 1; i <= 5; i++) {
System.out.println("Worker 2 - " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Get main thread reference
Thread mainThread = Thread.currentThread();
System.out.println("Main thread name: " + mainThread.getName());
System.out.println("Main thread priority: " + mainThread.getPriority());
// Set thread properties
worker1.setName("Worker-Thread-1");
worker2.setName("Worker-Thread-2");
worker1.setPriority(Thread.MAX_PRIORITY); // 10
worker2.setPriority(Thread.MIN_PRIORITY); // 1
worker1.start();
worker2.start();
// Demonstrate sleep
System.out.println("Main thread sleeping for 1 second...");
Thread.sleep(1000);
// Demonstrate interrupt
worker1.interrupt();
// Check if threads are alive
System.out.println("Worker1 alive: " + worker1.isAlive());
System.out.println("Worker2 alive: " + worker2.isAlive());
// Wait for worker2 to complete
worker2.join();
System.out.println("Worker2 completed, alive: " + worker2.isAlive());
// Demonstate yield
Thread.yield(); // Hint to scheduler to give other threads CPU time
System.out.println("Main thread finished");
}
}
5. Thread Synchronization
The Problem: Race Condition
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// Expected: 2000, but often gets less due to race condition
System.out.println("Final count: " + counter.getCount());
}
}
Solution 1: synchronized Method
class SynchronizedCounter {
private int count = 0;
// synchronized method
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// Always 2000
System.out.println("Final count: " + counter.getCount());
}
}
Solution 2: synchronized Block
class BlockCounter {
private int count = 0;
private final Object lock = new Object(); // Lock object
public void increment() {
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}
Solution 3: ReentrantLock
import java.util.concurrent.locks.*;
class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // Always unlock in finally block
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
6. Thread Communication - wait() and notify()
class SharedResource {
private int data;
private boolean available = false;
public synchronized void produce(int value) throws InterruptedException {
while (available) {
wait(); // Wait for consumer to consume
}
data = value;
available = true;
System.out.println("Produced: " + value);
notify(); // Notify consumer
}
public synchronized int consume() throws InterruptedException {
while (!available) {
wait(); // Wait for producer to produce
}
available = false;
System.out.println("Consumed: " + data);
notify(); // Notify producer
return data;
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
resource.produce(i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
resource.consume();
Thread.sleep(1500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7. Real-World Example: Bank Account Simulation
class BankAccount {
private double balance;
private final Object lock = new Object();
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
synchronized(lock) {
balance += amount;
System.out.println(Thread.currentThread().getName() +
" deposited: " + amount + ", Balance: " + balance);
lock.notifyAll(); // Notify waiting threads
}
}
public void withdraw(double amount) throws InterruptedException {
synchronized(lock) {
while (balance < amount) {
System.out.println(Thread.currentThread().getName() +
" waiting to withdraw: " + amount);
lock.wait(); // Wait for sufficient funds
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" withdrew: " + amount + ", Balance: " + balance);
}
}
public double getBalance() {
synchronized(lock) {
return balance;
}
}
}
public class BankAccountExample {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount(1000);
// Withdrawal threads
Thread withdraw1 = new Thread(() -> {
try {
account.withdraw(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Withdraw-1");
Thread withdraw2 = new Thread(() -> {
try {
account.withdraw(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Withdraw-2");
// Deposit threads
Thread deposit1 = new Thread(() -> {
account.deposit(500);
}, "Deposit-1");
Thread deposit2 = new Thread(() -> {
account.deposit(300);
}, "Deposit-2");
withdraw1.start();
withdraw2.start();
Thread.sleep(1000); // Let withdrawal threads wait
deposit1.start();
deposit2.start();
withdraw1.join();
withdraw2.join();
deposit1.join();
deposit2.join();
System.out.println("Final balance: " + account.getBalance());
}
}
8. Thread Pools with ExecutorService
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
System.out.println("=== Thread Pool Examples ===");
// 1. Fixed Thread Pool
ExecutorService fixedPool = Executors.newFixedThreadPool(3);
System.out.println("Fixed Thread Pool (3 threads):");
for (int i = 1; i <= 6; i++) {
final int taskId = i;
fixedPool.execute(() -> {
System.out.println(Thread.currentThread().getName() +
" executing task " + taskId);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
fixedPool.shutdown();
try {
fixedPool.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2. Cached Thread Pool
ExecutorService cachedPool = Executors.newCachedThreadPool();
System.out.println("\nCached Thread Pool:");
for (int i = 1; i <= 10; i++) {
final int taskId = i;
cachedPool.submit(() -> {
System.out.println(Thread.currentThread().getName() +
" executing task " + taskId);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
cachedPool.shutdown();
// 3. Scheduled Thread Pool
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
System.out.println("\nScheduled Thread Pool:");
scheduledPool.schedule(() -> {
System.out.println("Task executed after 2 seconds delay");
}, 2, TimeUnit.SECONDS);
scheduledPool.scheduleAtFixedRate(() -> {
System.out.println("Repeated task executed every 1 second");
}, 1, 1, TimeUnit.SECONDS);
// Let it run for 5 seconds then shutdown
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
scheduledPool.shutdown();
}
}
9. Callable and Future
import java.util.concurrent.*;
import java.util.*;
public class CallableFutureExample {
public static void main(String[] args) throws Exception {
System.out.println("=== Callable and Future ===");
ExecutorService executor = Executors.newFixedThreadPool(3);
// Callable that returns a result
Callable<Integer> factorialTask = () -> {
int number = 5;
int result = 1;
for (int i = 1; i <= number; i++) {
result *= i;
Thread.sleep(500); // Simulate computation
}
return result;
};
Callable<String> stringTask = () -> {
Thread.sleep(1000);
return "Hello from Callable!";
};
Callable<Double> randomTask = () -> {
Thread.sleep(800);
return Math.random() * 100;
};
// Submit tasks and get Futures
Future<Integer> factorialFuture = executor.submit(factorialTask);
Future<String> stringFuture = executor.submit(stringTask);
Future<Double> randomFuture = executor.submit(randomTask);
// Check if tasks are done
System.out.println("Factorial task done: " + factorialFuture.isDone());
System.out.println("String task done: " + stringFuture.isDone());
// Get results (blocks until available)
System.out.println("Factorial result: " + factorialFuture.get());
System.out.println("String result: " + stringFuture.get());
System.out.println("Random result: " + randomFuture.get());
// Multiple tasks with invokeAll
List<Callable<String>> tasks = Arrays.asList(
() -> { Thread.sleep(1000); return "Task 1"; },
() -> { Thread.sleep(500); return "Task 2"; },
() -> { Thread.sleep(800); return "Task 3"; }
);
System.out.println("\n=== invokeAll Example ===");
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println("Result: " + future.get());
}
executor.shutdown();
}
}
10. Common Multithreading Issues and Solutions
public class CommonIssues {
// 1. Deadlock Example
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void demonstrateDeadlock() {
Thread t1 = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized(lock2) {
System.out.println("Thread 1: Acquired both locks!");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized(lock1) {
System.out.println("Thread 2: Acquired both locks!");
}
}
});
t1.start();
t2.start();
}
// 2. Livelock Example
static class Spoon {
private Diner owner;
public Spoon(Diner d) { owner = d; }
public synchronized void setOwner(Diner d) { owner = d; }
public synchronized void use() {
System.out.println(owner.name + " is using the spoon!");
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String n) { name = n; isHungry = true; }
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) {
if (spoon.owner != this) {
try { Thread.sleep(1); } catch (InterruptedException e) {}
continue;
}
if (spouse.isHungry) {
System.out.println(name + ": You eat first, " + spouse.name);
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": I'm done eating");
spoon.setOwner(spouse);
}
}
}
public static void demonstrateLivelock() {
final Diner husband = new Diner("Husband");
final Diner wife = new Diner("Wife");
final Spoon spoon = new Spoon(husband);
new Thread(() -> husband.eatWith(spoon, wife)).start();
new Thread(() -> wife.eatWith(spoon, husband)).start();
}
}
public class IssuesDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Common Multithreading Issues ===");
// Uncomment to see deadlock (will freeze)
// CommonIssues.demonstrateDeadlock();
// Uncomment to see livelock
// CommonIssues.demonstrateLivelock();
// 3. Starvation Example
Object sharedResource = new Object();
Thread greedyThread = new Thread(() -> {
synchronized(sharedResource) {
while (true) {
try {
Thread.sleep(1000);
System.out.println("Greedy thread holding lock...");
} catch (InterruptedException e) {
break;
}
}
}
});
Thread starvedThread = new Thread(() -> {
// This thread will starve
synchronized(sharedResource) {
System.out.println("Starved thread finally got the lock!");
}
});
greedyThread.setDaemon(true); // So it doesn't prevent JVM exit
greedyThread.start();
Thread.sleep(100);
starvedThread.start();
// Let it run for a bit
Thread.sleep(3000);
starvedThread.interrupt();
}
}
11. Best Practices
public class MultithreadingBestPractices {
// 1. Use thread pools instead of creating threads manually
public static void useThreadPools() {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Executing task " + taskId +
" in " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
// 2. Prefer immutability
static final class ImmutableValue {
private final int value;
public ImmutableValue(int value) {
this.value = value;
}
public int getValue() { return value; }
public ImmutableValue add(int newValue) {
return new ImmutableValue(this.value + newValue);
}
}
// 3. Use concurrent collections
public static void useConcurrentCollections() {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// Thread-safe operations
map.put("key", 1);
list.add("value");
}
// 4. Proper resource cleanup
static class ResourceHandler implements AutoCloseable {
private final ExecutorService executor;
public ResourceHandler() {
this.executor = Executors.newFixedThreadPool(5);
}
public void executeTask(Runnable task) {
executor.submit(task);
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
public class BestPracticesDemo {
public static void main(String[] args) {
System.out.println("=== Best Practices ===");
// 1. Always use try-with-resources for cleanup
try (MultithreadingBestPractices.ResourceHandler handler =
new MultithreadingBestPractices.ResourceHandler()) {
handler.executeTask(() -> System.out.println("Task executed"));
} // Automatic cleanup
// 2. Use meaningful thread names
Thread worker = new Thread(() -> {
System.out.println("Working...");
}, "File-Processor-Thread");
worker.start();
// 3. Handle interrupts properly
Thread interruptibleThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("Working...");
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted, cleaning up...");
Thread.currentThread().interrupt(); // Preserve interrupt status
break;
}
}
});
interruptibleThread.start();
try {
Thread.sleep(3000);
interruptibleThread.interrupt();
interruptibleThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
12. Conclusion
Key Takeaways:
- Thread Creation: Extend Thread, implement Runnable, or use lambda
- Synchronization: Use synchronized blocks/methods or Lock objects
- Thread Communication: wait(), notify(), notifyAll()
- Thread Pools: Use ExecutorService for better resource management
- Callable/Future: For tasks that return results
Best Practices:
- ✅ Use thread pools instead of creating threads manually
- ✅ Prefer implementing Runnable over extending Thread
- ✅ Always use proper synchronization for shared resources
- ✅ Handle interrupts properly
- ✅ Use concurrent collections when possible
- ✅ Clean up resources properly
Common Pitfalls:
- ❌ Race conditions (missing synchronization)
- ❌ Deadlocks (circular wait for locks)
- ❌ Livelocks (threads busy but not progressing)
- ❌ Resource leakage (not shutting down executors)
- ❌ Ignoring interrupts
When to Use Multithreading:
- CPU-intensive tasks that can run in parallel
- I/O operations that involve waiting
- Background tasks in GUI applications
- Processing large datasets
- Handling multiple client requests
Final Thoughts:
Multithreading is a powerful feature but comes with complexity. Start with simple examples, understand the basics thoroughly, and always test your multithreaded code extensively. Modern Java provides excellent concurrency utilities - leverage them instead of reinventing the wheel.
Master multithreading to build high-performance, responsive applications that make the most of modern multi-core processors!