Multithreading
Threads, synchronization, and concurrency
1. Introduction to Multithreading
Single-Threaded Execution
- Tasks execute one after another
- CPU cores underutilized
- Blocking operations halt entire program
- Predictable but slow performance
- Simple to debug and understand
Multi-Threaded Execution
- Tasks execute concurrently
- Better CPU utilization across cores
- Non-blocking operations possible
- Complex but efficient performance
- Requires careful synchronization
Sequential vs Concurrent Execution
// Sequential execution example
public class SequentialExample {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// Task 1 - takes 3 seconds
performTask("Database Operation", 3000);
// Task 2 - takes 2 seconds
performTask("File Processing", 2000);
// Task 3 - takes 1 second
performTask("Network Call", 1000);
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start) + "ms");
// Output: Total time: ~6000ms
}
static void performTask(String taskName, int duration) {
System.out.println("Starting " + taskName);
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Completed " + taskName);
}
}
// Concurrent execution example
public class ConcurrentExample {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
// Create threads for each task
Thread task1 = new Thread(() -> performTask("Database Operation", 3000));
Thread task2 = new Thread(() -> performTask("File Processing", 2000));
Thread task3 = new Thread(() -> performTask("Network Call", 1000));
// Start all threads simultaneously
task1.start();
task2.start();
task3.start();
// Wait for all to complete
task1.join();
task2.join();
task3.join();
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start) + "ms");
// Output: Total time: ~3000ms (limited by slowest task)
}
}
Benefits of Multithreading
- Improved application responsiveness
- Better resource utilization
- Concurrent task execution
- Enhanced user experience
- Scalability on multi-core systems
Common Use Cases
- GUI applications (UI + background tasks)
- Web servers (handling multiple requests)
- Data processing pipelines
- Producer-consumer scenarios
- Parallel algorithms
Challenges to Consider
- Race conditions and data corruption
- Deadlocks and livelocks
- Resource contention
- Debugging complexity
- Memory consistency issues
2. Threading Basics
Process vs Thread
- Process: Independent program with own memory space
- Thread: Lightweight process sharing memory within process
- Threads share heap memory but have separate stacks
- Context switching between threads is faster than processes
Thread Components
- Program Counter: Points to current instruction
- Stack: Local variables and method calls
- Registers: CPU register values
- State: Current execution state
- Priority: Scheduling priority level
Thread Types in Java
- User Threads: Normal application threads
- Daemon Threads: Background service threads
- Main Thread: Entry point of application
- Worker Threads: Execute specific tasks
Thread Information and Properties
public class ThreadBasicsDemo {
public static void main(String[] args) {
// Get information about current thread
Thread currentThread = Thread.currentThread();
System.out.println("=== Current Thread Information ===");
System.out.println("Name: " + currentThread.getName());
System.out.println("ID: " + currentThread.getId());
System.out.println("Priority: " + currentThread.getPriority());
System.out.println("State: " + currentThread.getState());
System.out.println("Is Alive: " + currentThread.isAlive());
System.out.println("Is Daemon: " + currentThread.isDaemon());
// System information
System.out.println("\n=== System Information ===");
System.out.println("Available Processors: " +
Runtime.getRuntime().availableProcessors());
System.out.println("Active Thread Count: " +
Thread.activeCount());
// Create and examine a custom thread
Thread customThread = new Thread(() -> {
System.out.println("\n=== Custom Thread Executing ===");
System.out.println("Custom thread name: " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Custom thread work completed");
}, "MyCustomThread");
// Set thread properties
customThread.setPriority(Thread.NORM_PRIORITY + 1);
customThread.setDaemon(false);
System.out.println("\n=== Before Start ===");
System.out.println("State: " + customThread.getState());
customThread.start();
System.out.println("\n=== After Start ===");
System.out.println("State: " + customThread.getState());
try {
customThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("\n=== After Completion ===");
System.out.println("State: " + customThread.getState());
}
}
3. Thread Creation Methods
Method 1: Extending Thread Class
class CustomThread extends Thread {
private String taskName;
private int iterations;
public CustomThread(String taskName, int iterations) {
super(taskName);
this.taskName = taskName;
this.iterations = iterations;
}
@Override
public void run() {
System.out.println(taskName + " started by " + getName());
for (int i = 1; i " - Iteration " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.println(taskName + " completed");
}
}
// Usage
CustomThread thread1 = new CustomThread("DataProcessor", 3);
thread1.start();
Method 2: Implementing Runnable Interface (Recommended)
class TaskRunner implements Runnable {
private String taskName;
private int delay;
public TaskRunner(String taskName, int delay) {
this.taskName = taskName;
this.delay = delay;
}
@Override
public void run() {
System.out.println(taskName + " started in thread: " +
Thread.currentThread().getName());
try {
Thread.sleep(delay);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Task was interrupted");
}
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted");
Thread.currentThread().interrupt();
}
System.out.println(taskName + " finished");
}
}
// Usage
TaskRunner task = new TaskRunner("EmailSender", 800);
Thread thread = new Thread(task, "Email-Thread");
thread.start();
Method 3: Using ExecutorService (Production Recommended)
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
public class ExecutorServiceExample {
public static void main(String[] args) {
// Create different types of thread pools
ExecutorService fixedPool = Executors.newFixedThreadPool(3);
ExecutorService cachedPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
try {
// Submit Runnable tasks
for (int i = 1; i 5; i++) {
final int taskId = i;
fixedPool.submit(() -> {
System.out.println("Task " + taskId + " running in: " +
Thread.currentThread().getName());
try { Thread.sleep(2000); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Task " + taskId + " completed");
});
}
// Submit Callable tasks with return values
List<Future<String>> futures = new ArrayList<>();
for (int i = 1; i 4; i++) {
final int taskId = i;
Future<String> future = cachedPool.submit(() -> {
Thread.sleep(1000 + taskId * 300);
return "Result from task " + taskId;
});
futures.add(future);
}
// Collect results with timeout
for (int i = 0; i try {
String result = futures.get(i).get(3, TimeUnit.SECONDS);
System.out.println(result);
} catch (TimeoutException e) {
System.out.println("Task timed out");
futures.get(i).cancel(true);
} catch (ExecutionException e) {
System.out.println("Task failed: " + e.getCause());
}
}
// Scheduled tasks
scheduledPool.scheduleWithFixedDelay(() -> {
System.out.println("Periodic task at: " + System.currentTimeMillis());
}, 0, 1, TimeUnit.SECONDS);
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// Proper shutdown
fixedPool.shutdown();
cachedPool.shutdown();
scheduledPool.shutdown();
}
}
}
| Method | Pros | Cons | Best Use Case |
|---|---|---|---|
| Extend Thread | Simple, direct control | No multiple inheritance | Simple standalone threads |
| Implement Runnable | Better OOP design, flexible | More verbose | When inheritance flexibility needed |
| ExecutorService | Thread pool management | More complex API | Production applications |
| CompletableFuture | Modern, composable | Learning curve | Async pipelines |
4. Thread Lifecycle and States
NEW
Thread created but not started
Created with new Thread() but start() not called
RUNNABLE
Thread executing or ready to execute
Either running or waiting for CPU time
BLOCKED
Thread blocked waiting for monitor lock
Waiting to enter synchronized block
WAITING
Thread waiting indefinitely
Called wait(), join(), or LockSupport.park()
TIMED_WAITING
Thread waiting for specified time
Called sleep(), wait(timeout), or join(timeout)
TERMINATED
Thread completed execution
run() method finished or exception occurred
Thread Lifecycle Demonstration
public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Object sharedLock = new Object();
Thread workerThread = new Thread(() -> {
try {
System.out.println("Worker: Starting execution");
// Demonstrate TIMED_WAITING state
System.out.println("Worker: Going to sleep");
Thread.sleep(2000);
System.out.println("Worker: Woke up");
// Demonstrate WAITING state
synchronized (sharedLock) {
System.out.println("Worker: Waiting for notification");
sharedLock.wait();
System.out.println("Worker: Received notification");
}
System.out.println("Worker: Completing execution");
} catch (InterruptedException e) {
System.out.println("Worker: Was interrupted");
Thread.currentThread().interrupt();
}
}, "WorkerThread");
// NEW state
System.out.println("Initial state: " + workerThread.getState());
workerThread.start();
// Let worker enter sleep state
Thread.sleep(1000);
System.out.println("During sleep: " + workerThread.getState());
// Let worker enter wait state
Thread.sleep(2000);
System.out.println("During wait: " + workerThread.getState());
// Notify worker
Thread.sleep(1000);
synchronized (sharedLock) {
System.out.println("Main: Notifying worker");
sharedLock.notify();
}
workerThread.join();
System.out.println("Final state: " + workerThread.getState());
}
}
Best Practice
Always use cooperative interruption rather than deprecated methods like stop(). Check Thread.currentThread().isInterrupted() regularly in long-running loops and handle InterruptedException properly.
5. Synchronization Mechanisms
The Race Condition Problem
class UnsafeCounter {
private int count = 0;
public void increment() {
// This operation is NOT atomic!
// 1. Read current value of count
// 2. Add 1 to the value
// 3. Write the result back to count
count++;
}
public int getCount() {
return count;
}
}
public class RaceConditionDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
int numThreads = 10;
int incrementsPerThread = 1000;
Thread[] threads = new Thread[numThreads];
for (int i = 0; i new Thread(() -> {
for (int j = 0; j for (Thread thread : threads) thread.start();
for (Thread thread : threads) thread.join();
int expected = numThreads * incrementsPerThread;
System.out.println("Expected: " + expected);
System.out.println("Actual: " + counter.getCount());
System.out.println("Lost updates: " + (expected - counter.getCount()));
}
}
Synchronized Solution
class SafeCounter {
private int count = 0;
private final Object lock = new Object();
// Synchronized method - uses intrinsic lock
public synchronized void increment() {
count++;
}
// Synchronized block with custom lock object
public void decrement() {
synchronized (lock) {
count--;
}
}
public synchronized int getCount() {
return count;
}
}
// Thread-safe bank account example
class BankAccount {
private double balance;
private final Object balanceLock = new Object();
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount 0) throw new IllegalArgumentException("Amount must be positive");
synchronized (balanceLock) {
double oldBalance = balance;
balance += amount;
System.out.println(Thread.currentThread().getName() +
" deposited $" + amount +
" (Balance: $" + oldBalance + " -> $" + balance + ")");
}
}
public boolean withdraw(double amount) {
synchronized (balanceLock) {
if (balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" withdrew $" + amount);
return true;
}
return false;
}
}
public synchronized double getBalance() {
return balance;
}
}
6. Concurrent Collections
ConcurrentHashMap Example
import java.util.concurrent.*;
public class ConcurrentHashMapDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("globalCounter", 0);
Thread[] threads = new Thread[5];
for (int i = 0; i 5; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j 1000; j++) {
// Atomic increment using compute
concurrentMap.compute("globalCounter", (key, val) ->
val == null ? 1 : val + 1);
// Atomic addition using merge
concurrentMap.merge("thread-" + threadId, 1, Integer::sum);
}
});
}
for (Thread thread : threads) thread.start();
for (Thread thread : threads) thread.join();
System.out.println("Global counter: " + concurrentMap.get("globalCounter"));
System.out.println("Expected: 5000");
// Safe iteration
concurrentMap.forEach((key, value) -> System.out.println(key + " = " + value));
}
}
BlockingQueue - Producer-Consumer Pattern
import java.util.concurrent.*;
public class BlockingQueueDemo {
private static final String POISON_PILL = "STOP";
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// Producer
Thread producer = new Thread(() -> {
try {
for (int i = 1; i 20; i++) {
String item = "Task-" + i;
System.out.println("[Producer] Creating: " + item);
queue.put(item); // Blocks if queue is full
Thread.sleep(150);
}
queue.put(POISON_PILL);
queue.put(POISON_PILL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
// Consumers
Thread consumer1 = new Thread(() -> consume(queue), "Consumer-1");
Thread consumer2 = new Thread(() -> consume(queue), "Consumer-2");
producer.start();
consumer1.start();
consumer2.start();
producer.join();
consumer1.join();
consumer2.join();
System.out.println("Demo completed");
}
private static void consume(BlockingQueue<String> queue) {
try {
while (true) {
String item = queue.take(); // Blocks if queue is empty
if (item.equals(POISON_PILL)) {
System.out.println("[" + Thread.currentThread().getName() + "] Stopping");
break;
}
System.out.println("[" + Thread.currentThread().getName() + "] Processing: " + item);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
| Collection | Use Case | Read Performance | Write Performance |
|---|---|---|---|
| ConcurrentHashMap | High concurrency read/write | Excellent | Excellent |
| CopyOnWriteArrayList | Read-heavy, rare writes | Excellent | Poor |
| ArrayBlockingQueue | Bounded producer-consumer | Good | Good |
| LinkedBlockingQueue | Unbounded producer-consumer | Good | Good |
7. Advanced Concurrency Utilities
CountDownLatch - Service Startup Coordination
import java.util.concurrent.*;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int numberOfServices = 4;
CountDownLatch allServicesReady = new CountDownLatch(numberOfServices);
String[] services = {"Database", "Cache", "Web", "Security"};
int[] startupTimes = {2000, 1500, 1000, 2500};
System.out.println("=== Application Startup ===");
long start = System.currentTimeMillis();
for (int i = 0; i final int index = i;
new Thread(() -> {
try {
System.out.println(services[index] + " starting...");
Thread.sleep(startupTimes[index]);
System.out.println(services[index] + " started");
allServicesReady.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
System.out.println("Waiting for all services...");
allServicesReady.await();
long end = System.currentTimeMillis();
System.out.println("All services started! Total time: " + (end - start) + "ms");
}
}
Semaphore - Resource Pool Management
import java.util.concurrent.*;
class ConnectionPool {
private final Semaphore availableConnections;
private final int maxConnections;
public ConnectionPool(int maxConnections) {
this.maxConnections = maxConnections;
this.availableConnections = new Semaphore(maxConnections, true);
}
public void getConnection() throws InterruptedException {
availableConnections.acquire();
System.out.println("[" + Thread.currentThread().getName() +
"] Acquired connection (available: " +
availableConnections.availablePermits() + ")");
}
public void releaseConnection() {
System.out.println("[" + Thread.currentThread().getName() +
"] Released connection");
availableConnections.release();
}
}
public class SemaphoreDemo {
public static void main(String[] args) throws InterruptedException {
ConnectionPool pool = new ConnectionPool(3);
int numberOfClients = 8;
Thread[] clients = new Thread[numberOfClients];
for (int i = 0; i new Thread(() -> {
try {
pool.getConnection();
Thread.sleep(1000 + (int)(Math.random() * 2000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
pool.releaseConnection();
}
}, "Client-" + i);
}
for (Thread client : clients) client.start();
for (Thread client : clients) client.join();
System.out.println("All clients completed");
}
}
Performance Tip
Use fair semaphores (true parameter) when you need to prevent thread starvation, but note that fair semaphores have slightly lower performance than unfair ones.
8. Best Practices and Performance
Thread Safety Principles
- Prefer immutable objects when possible
- Minimize shared mutable state
- Use thread-safe collections
- Avoid shared variables between threads
- Use defensive copying for mutable objects
Performance Guidelines
- Use thread pools instead of creating threads
- Choose appropriate pool sizes
- Avoid excessive synchronization
- Use lock-free algorithms when possible
- Profile and measure performance
Error Handling
- Always handle InterruptedException properly
- Use UncaughtExceptionHandler for critical threads
- Implement proper shutdown sequences
- Use timeouts to avoid infinite waits
- Log thread-related errors with context
Production Thread Pool Configuration
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ProductionThreadPoolExample {
// Custom thread factory for better debugging
private static class NamedThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public NamedThreadFactory(String namePrefix) {
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
thread.setUncaughtExceptionHandler((t, e) -> {
System.err.println("Thread " + t.getName() + " threw: " + e.getMessage());
});
return thread;
}
}
public static void main(String[] args) throws InterruptedException {
// CPU-bound task pool
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor cpuBoundPool = new ThreadPoolExecutor(
cpuCores,
cpuCores,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new NamedThreadFactory("CPU-Worker")
);
// I/O-bound task pool
ThreadPoolExecutor ioBoundPool = new ThreadPoolExecutor(
10,
50,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new NamedThreadFactory("IO-Worker"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
// Submit tasks...
for (int i = 0; i 10; i++) {
final int taskId = i;
cpuBoundPool.submit(() -> {
System.out.println("CPU Task " + taskId + " running");
});
}
Thread.sleep(5000);
} finally {
shutdownGracefully(cpuBoundPool, "CPU Pool");
shutdownGracefully(ioBoundPool, "I/O Pool");
}
}
private static void shutdownGracefully(ExecutorService executor, String name) {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
9. Interview Questions
Conceptual Questions
- Q: What is the difference between process and thread?
- Q: Explain synchronized vs volatile keywords.
- Q: What is a race condition and how to prevent it?
- Q: When to use CountDownLatch vs CyclicBarrier?
Coding Questions
- Q: Implement a thread-safe singleton.
- Q: Create a producer-consumer solution.
- Q: Print numbers 1-100 using 3 threads alternately.
- Q: Implement a thread-safe counter.
Advanced Questions
- Q: How does ConcurrentHashMap achieve thread safety?
- Q: Explain happens-before relationship.
- Q: How to detect and prevent deadlocks?
- Q: What are lock-free algorithms?
Sample Interview Coding Problem
// Problem: Implement a thread-safe counter with increment,
// decrement, and get operations.
import java.util.concurrent.atomic.AtomicInteger;
// Solution 1: Using AtomicInteger (preferred)
class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public void decrement() {
count.decrementAndGet();
}
public int get() {
return count.get();
}
}
// Test
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
int numThreads = 10;
int ops = 1000;
Thread[] threads = new Thread[numThreads];
for (int i = 0; i if (i % 2 == 0) {
threads[i] = new Thread(() -> {
for (int j = 0; j else {
threads[i] = new Thread(() -> {
for (int j = 0; j for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
System.out.println("Final value: " + counter.get());
System.out.println("Expected: 0");
}
}
Interview Success Tips
- Understand Fundamentals: Know thread lifecycle, states, and basic synchronization primitives
- Practice Coding: Implement common patterns like producer-consumer and thread pools
- Know Collections: Understand when to use ConcurrentHashMap vs CopyOnWriteArrayList
- Explain Trade-offs: Discuss performance vs complexity and safety vs speed
- Real-world Examples: Be ready to discuss practical applications