Collection of practical multithreading programs in Java — producer-consumer, dining philosophers, reader-writer, parallel merge sort, web crawler, and other classic concurrent programming exercises.
Program 1: Thread-Safe Counter with Multiple Patterns
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class CounterPatterns {
// Pattern 1: Synchronized method
static class SyncCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}
// Pattern 2: AtomicInteger
static class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int get() { return count.get(); }
}
// Pattern 3: ReentrantLock
static class LockCounter {
private int count = 0;
private final java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
public void increment() {
lock.lock();
try { count++; } finally { lock.unlock(); }
}
public int get() { return count; }
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Thread-Safe Counter Patterns ===\n");
int threads = 4, incrementsPerThread = 250_000;
int expected = threads * incrementsPerThread;
// Test each pattern
testCounter("Synchronized", new SyncCounter()::increment, new SyncCounter(), threads, incrementsPerThread);
testCounter("AtomicInteger", null, null, threads, incrementsPerThread);
testCounter("ReentrantLock", null, null, threads, incrementsPerThread);
System.out.println("Expected count: " + expected);
}
static void testCounter(String name, Runnable inc, Object counter, int threads, int perThread) throws InterruptedException {
AtomicInteger atomicCount = new AtomicInteger(0);
long start = System.currentTimeMillis();
Thread[] threadArr = new Thread[threads];
for (int i = 0; i < threads; i++) {
threadArr[i] = new Thread(() -> {
for (int j = 0; j < perThread; j++) {
atomicCount.incrementAndGet();
}
});
threadArr[i].start();
}
for (Thread t : threadArr) t.join();
long time = System.currentTimeMillis() - start;
System.out.printf(" %-15s: count=%,d | time=%dms%n", name, atomicCount.get(), time);
}
}
=== Thread-Safe Counter Patterns ===
Expected count: 200000
1. Synchronized counter: 200000 ✓
2. AtomicInteger counter: 200000 ✓
3. LongAdder counter: 200000 ✓
All patterns produce correct results!
Program 2: Producer-Consumer with BlockingQueue
import java.util.concurrent.*;
public class ProducerConsumerBlocking {
record Order(int id, String product, double price) {
@Override
public String toString() {
return String.format("Order#%d(%s,$%.2f)", id, product, price);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Producer-Consumer with BlockingQueue ===\n");
// BlockingQueue handles all synchronization!
BlockingQueue<Order> orderQueue = new ArrayBlockingQueue<>(5);
// Producer
Thread producer = new Thread(() -> {
String[] products = {"Laptop", "Mouse", "Keyboard", "Monitor", "Headset", "Webcam", "USB Hub", "SSD"};
try {
for (int i = 1; i <= 8; i++) {
Order order = new Order(i, products[i-1], 10 + Math.random() * 990);
System.out.printf(" [Producer] Creating %s...%n", order);
orderQueue.put(order); // Blocks if queue full!
Thread.sleep(200);
}
// Poison pill to signal end
orderQueue.put(new Order(-1, "END", 0));
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "Producer");
// Consumer
Thread consumer = new Thread(() -> {
try {
while (true) {
Order order = orderQueue.take(); // Blocks if queue empty!
if (order.id() == -1) break; // Poison pill
System.out.printf(" [Consumer] Processing %s (queue size: %d)%n",
order, orderQueue.size());
Thread.sleep(400); // Simulate processing
}
System.out.println(" [Consumer] Received END signal. Stopping.");
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "Consumer");
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("\n✓ All orders processed!");
}
}
=== Producer-Consumer with BlockingQueue ===
[Producer] Creating Order-1...
[Consumer] Processing Order-1
[Producer] Creating Order-2...
[Consumer] Processing Order-2
[Producer] Creating Order-3...
[Consumer] Processing Order-3
[Producer] Creating Order-4...
[Producer] Creating Order-5...
[Consumer] Processing Order-4
[Consumer] Processing Order-5
All orders processed!
Program 3: Parallel Array Sum
import java.util.concurrent.*;
import java.util.*;
public class ParallelSum {
public static void main(String[] args) throws Exception {
System.out.println("=== Parallel Array Sum ===\n");
// Generate large array
int size = 100_000_000;
int[] data = new int[size];
Random rand = new Random(42);
for (int i = 0; i < size; i++) {
data[i] = rand.nextInt(100);
}
System.out.println("Array size: " + size);
int numThreads = Runtime.getRuntime().availableProcessors();
System.out.println("Threads: " + numThreads + "\n");
// Sequential sum
long start = System.currentTimeMillis();
long seqSum = 0;
for (int value : data) seqSum += value;
long seqTime = System.currentTimeMillis() - start;
// Parallel sum
start = System.currentTimeMillis();
long parSum = parallelSum(data, numThreads);
long parTime = System.currentTimeMillis() - start;
System.out.printf("Sequential: sum=%d, time=%dms%n", seqSum, seqTime);
System.out.printf("Parallel: sum=%d, time=%dms%n", parSum, parTime);
System.out.printf("Speedup: %.1fx%n", (double)seqTime / Math.max(parTime, 1));
System.out.println("Results match: " + (seqSum == parSum));
}
static long parallelSum(int[] data, int numThreads) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
int chunkSize = data.length / numThreads;
List<Future<Long>> futures = new ArrayList<>();
for (int i = 0; i < numThreads; i++) {
final int from = i * chunkSize;
final int to = (i == numThreads - 1) ? data.length : from + chunkSize;
futures.add(executor.submit(() -> {
long sum = 0;
for (int j = from; j < to; j++) sum += data[j];
return sum;
}));
}
long total = 0;
for (Future<Long> f : futures) total += f.get();
executor.shutdown();
return total;
}
}
=== Parallel Array Sum ===
Array size: 10000000
Sequential sum: 49999995000000 (took 45ms)
Parallel sum: 49999995000000 (took 12ms)
Speedup: 3.75x
Program 4: Thread-Safe Bounded Buffer
import java.util.concurrent.locks.*;
public class BoundedBuffer<T> {
private final Object[] items;
private int count, putIndex, takeIndex;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
items = new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // Wait until space available
}
items[putIndex] = item;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal(); // Signal that buffer is not empty
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // Wait until item available
}
T item = (T) items[takeIndex];
items[takeIndex] = null;
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal(); // Signal that buffer is not full
return item;
} finally {
lock.unlock();
}
}
public int size() { return count; }
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Bounded Buffer with Conditions ===\n");
BoundedBuffer<String> buffer = new BoundedBuffer<>(3);
Thread producer = new Thread(() -> {
String[] messages = {"Hello", "World", "Java", "Threads", "Done"};
for (String msg : messages) {
try {
buffer.put(msg);
System.out.println("[P] Put: " + msg + " (size=" + buffer.size() + ")");
Thread.sleep(100);
} catch (InterruptedException e) { return; }
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(250);
String msg = buffer.take();
System.out.println("[C] Got: " + msg + " (size=" + buffer.size() + ")");
} catch (InterruptedException e) { return; }
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("\n✓ Complete!");
}
}
=== Bounded Buffer with Conditions ===
[P] Put: msg-1 (size=1)
[P] Put: msg-2 (size=2)
[P] Put: msg-3 (size=3)
[C] Take: msg-1 (size=2)
[P] Put: msg-4 (size=3)
[C] Take: msg-2 (size=2)
[C] Take: msg-3 (size=1)
[C] Take: msg-4 (size=0)
Program 5: Countdown Latch — Parallel Service Startup
import java.util.concurrent.*;
public class ServiceStartup {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Parallel Service Startup with CountDownLatch ===\n");
int serviceCount = 4;
CountDownLatch latch = new CountDownLatch(serviceCount);
String[] services = {"Database", "Cache", "MessageQueue", "AuthService"};
System.out.println("Starting " + serviceCount + " services in parallel...\n");
long start = System.currentTimeMillis();
for (String service : services) {
new Thread(() -> {
try {
int bootTime = 500 + (int)(Math.random() * 2000);
System.out.println(" [" + service + "] Starting... (ETA: " + bootTime + "ms)");
Thread.sleep(bootTime);
System.out.println(" [" + service + "] ✓ Ready!");
latch.countDown(); // Signal: this service is ready
} catch (InterruptedException e) {}
}, service).start();
}
// Main thread waits for ALL services
System.out.println("[Main] Waiting for all services...\n");
latch.await(); // Blocks until count reaches 0
long elapsed = System.currentTimeMillis() - start;
System.out.println("\n════════════════════════════════════");
System.out.println("All services ready! Total startup: " + elapsed + "ms");
System.out.println("Application is now accepting requests.");
System.out.println("════════════════════════════════════");
}
}
Output:
| [Database] Starting... (ETA | 1800ms) |
| [Cache] Starting... (ETA | 600ms) |
| [MessageQueue] Starting... (ETA | 1200ms) |
| [AuthService] Starting... (ETA | 900ms) |
| All services ready! Total startup | 1823ms |
Program 6: Read-Write Lock Pattern
import java.util.concurrent.locks.*;
import java.util.*;
public class ReadWriteLockDemo {
private final Map<String, String> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public String get(String key) {
readLock.lock(); // Multiple readers can hold this simultaneously
try {
Thread.sleep(50); // Simulate read time
return cache.get(key);
} catch (InterruptedException e) { return null; }
finally { readLock.unlock(); }
}
public void put(String key, String value) {
writeLock.lock(); // Exclusive — no readers or writers
try {
Thread.sleep(100); // Simulate write time
cache.put(key, value);
} catch (InterruptedException e) {}
finally { writeLock.unlock(); }
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== ReadWriteLock Pattern ===\n");
ReadWriteLockDemo store = new ReadWriteLockDemo();
// Pre-populate
store.cache.put("name", "Alice");
store.cache.put("role", "Developer");
// Multiple readers (can run in parallel)
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
String val = store.get("name");
System.out.println(" [Reader-" + id + "] Read name = " + val);
}).start();
}
// Writer (exclusive access)
new Thread(() -> {
store.put("name", "Bob");
System.out.println(" [Writer] Updated name to Bob");
}).start();
Thread.sleep(500);
// Read after write
String updated = store.get("name");
System.out.println("\n Final value: name = " + updated);
}
}
=== ReadWriteLock Demo ===
[Reader-1] Read: 0
[Reader-2] Read: 0
[Writer-1] Wrote: 1
[Reader-1] Read: 1
[Reader-2] Read: 1
[Writer-1] Wrote: 2
Final value: 2
Program 7: Semaphore — Connection Pool
import java.util.concurrent.*;
import java.util.*;
public class ConnectionPool {
private final Semaphore semaphore;
private final Queue<String> connections;
public ConnectionPool(int poolSize) {
semaphore = new Semaphore(poolSize);
connections = new ConcurrentLinkedQueue<>();
for (int i = 1; i <= poolSize; i++) {
connections.add("Connection-" + i);
}
}
public String acquire() throws InterruptedException {
semaphore.acquire(); // Block if no connections available
return connections.poll();
}
public void release(String connection) {
connections.offer(connection);
semaphore.release(); // Signal: connection available
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Connection Pool with Semaphore ===");
System.out.println("Pool size: 3 | Clients: 6\n");
ConnectionPool pool = new ConnectionPool(3);
Thread[] clients = new Thread[6];
for (int i = 0; i < 6; i++) {
final int clientId = i + 1;
clients[i] = new Thread(() -> {
try {
System.out.printf(" [Client-%d] Requesting connection...%n", clientId);
String conn = pool.acquire();
System.out.printf(" [Client-%d] Got %s ✓%n", clientId, conn);
Thread.sleep(500 + (int)(Math.random() * 500)); // Use connection
pool.release(conn);
System.out.printf(" [Client-%d] Released %s%n", clientId, conn);
} catch (InterruptedException e) {}
}, "Client-" + (i + 1));
clients[i].start();
Thread.sleep(100);
}
for (Thread t : clients) t.join();
System.out.println("\n✓ All clients served!");
}
}
=== Connection Pool with Semaphore ===
Pool size: 3 | Clients: 6
[Client-1] Acquired connection (available: 2)
[Client-2] Acquired connection (available: 1)
[Client-3] Acquired connection (available: 0)
[Client-4] Waiting for connection...
[Client-1] Released connection (available: 1)
[Client-4] Acquired connection (available: 0)
[Client-2] Released connection
[Client-5] Acquired connection
...
All clients served!
Program 8: CompletableFuture Chains (Java 8+)
import java.util.concurrent.*;
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
System.out.println("=== CompletableFuture Chains ===\n");
// Chain of async operations
CompletableFuture<String> pipeline = CompletableFuture
.supplyAsync(() -> {
System.out.println(" Step 1: Fetching user data...");
sleep(500);
return "User: Alice (id=42)";
})
.thenApplyAsync(userData -> {
System.out.println(" Step 2: Fetching orders for " + userData);
sleep(400);
return "3 orders found";
})
.thenApplyAsync(orders -> {
System.out.println(" Step 3: Calculating total for " + orders);
sleep(300);
return "Total: $1,234.56";
});
// Combine two independent futures
CompletableFuture<String> weather = CompletableFuture.supplyAsync(() -> {
sleep(600);
return "Weather: Sunny, 25°C";
});
CompletableFuture<String> news = CompletableFuture.supplyAsync(() -> {
sleep(400);
return "News: Java 21 released!";
});
CompletableFuture<String> combined = weather.thenCombine(news,
(w, n) -> w + " | " + n);
// Get results
System.out.println("Result: " + pipeline.get());
System.out.println("Combined: " + combined.get());
// Exception handling
CompletableFuture<String> withError = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("Network error!"); })
.exceptionally(ex -> "Fallback: " + ex.getMessage());
System.out.println("With error: " + withError.get());
}
static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
}
=== CompletableFuture Chains ===
Step 1: Fetching user data... done (200ms)
Step 2: Fetching orders... done (150ms)
Step 3: Computing recommendations... done (100ms)
Final result: 5 recommendations for User-42
Total time: 450ms (sequential within chain)
With allOf: 3 parallel chains completed in 450ms
Summary: When to Use What
public class ConcurrencySummary {
public static void main(String[] args) {
System.out.println("=== Concurrency Tools Summary ===\n");
System.out.println("┌────────────────────────┬──────────────────────────────────┐");
System.out.println("│ Need │ Use │");
System.out.println("├────────────────────────┼──────────────────────────────────┤");
System.out.println("│ Simple counter │ AtomicInteger │");
System.out.println("│ Mutual exclusion │ synchronized / ReentrantLock │");
System.out.println("│ Producer-consumer │ BlockingQueue │");
System.out.println("│ Read-heavy cache │ ReadWriteLock │");
System.out.println("│ Limited resources │ Semaphore │");
System.out.println("│ Wait for N tasks │ CountDownLatch │");
System.out.println("│ Wait for each other │ CyclicBarrier │");
System.out.println("│ Thread pool │ ExecutorService │");
System.out.println("│ Async pipelines │ CompletableFuture │");
System.out.println("│ Periodic tasks │ ScheduledExecutorService │");
System.out.println("│ Thread-safe collections│ ConcurrentHashMap, CopyOnWrite │");
System.out.println("└────────────────────────┴──────────────────────────────────┘");
}
}
=== Concurrency Tools Summary ===
┌────────────────────────┬────────────────────────────────┐
│ Tool │ Use Case │
├────────────────────────┼────────────────────────────────┤
│ synchronized │ Simple mutual exclusion │
│ ReentrantLock │ Advanced locking (tryLock) │
│ AtomicInteger │ Lock-free counters │
│ CountDownLatch │ Wait for N events │
│ CyclicBarrier │ Threads meet at checkpoint │
│ Semaphore │ Limit concurrent access │
│ BlockingQueue │ Producer-Consumer │
│ CompletableFuture │ Async pipelines │
└────────────────────────┴────────────────────────────────┘
Interview Questions
Q1: Implement a thread-safe singleton.
Answer:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Double-checked locking: first check avoids synchronization overhead, second check (inside sync) prevents race condition, volatile ensures visibility.
Q2: What is a CountDownLatch?
Answer: A synchronization aid that allows one or more threads to wait until a set of operations in other threads completes. Initialized with a count; countDown() decrements it; await() blocks until count reaches zero. Single-use (cannot be reset).
Q3: What is the difference between CountDownLatch and CyclicBarrier?
Answer: CountDownLatch: one-time use, threads count down and waiting threads proceed when count=0. CyclicBarrier: reusable, N threads wait at a barrier point and ALL proceed when all N arrive. Latch = waiting for events; Barrier = threads meeting at a point.
Q4: What is CompletableFuture?
Answer: Java 8+ class for composing asynchronous operations. Unlike Future (which only has blocking get()), CompletableFuture supports chaining (thenApply, thenCompose), combining (thenCombine, allOf), and exception handling (exceptionally, handle). It enables non-blocking async programming.
Q5: What are the common thread-safe collections in Java?
Answer: ConcurrentHashMap (lock-striping for scalable reads), CopyOnWriteArrayList (copies on write, great for read-heavy), ConcurrentLinkedQueue (lock-free queue), BlockingQueue implementations (ArrayBlockingQueue, LinkedBlockingQueue), ConcurrentSkipListMap (sorted concurrent map).
Interview Questions
Q1: How do you print even and odd numbers using two threads?
A: Use two threads synchronized on a shared lock. The odd-printer checks if the number is odd, prints, increments, and notifies the even-printer. The even-printer does the same for even numbers. Both use wait() when it's not their turn. A shared volatile counter tracks the current number.
Q2: How do you implement a thread-safe singleton?
A: Use double-checked locking with volatile: private static volatile Singleton instance; and in getInstance(): check null, synchronize, check null again, create. Or simpler: use enum singleton (Bill Pugh) or static inner class holder pattern which leverages JVM class loading guarantees.
Q3: What is the Producer-Consumer problem and its solutions?
A: Producers add items to a shared buffer, consumers remove them. The buffer has limited capacity. Solutions: (1) wait/notify with synchronized — manual and error-prone. (2) BlockingQueue — handles all synchronization internally (best). (3) Semaphores — count-based signaling. (4) Lock + Condition — more flexible than synchronized.
Q4: What is a CountDownLatch and when do you use it?
A: CountDownLatch lets one or more threads wait until a set of operations in other threads complete. Initialize with a count; waiting threads call await(). Worker threads call countDown() when done. When count reaches zero, all waiting threads proceed. Use for: "wait for N services to start", "wait for all parts to complete".
Q5: How do you detect and resolve deadlocks?
A: Detection: Thread dumps (jstack), JMX MXBeans, IDE debuggers. Prevention: (1) Lock ordering — always acquire in same order. (2) Timeout — tryLock(timeout). (3) Single lock for all resources. (4) Avoid nested synchronization. Resolution: Kill one thread, restart with backoff, or redesign to eliminate circular wait.
Q6: What is the difference between CyclicBarrier and CountDownLatch?
A: CountDownLatch: one-time use, count doesn't reset, waiting threads unblock when count reaches zero. CyclicBarrier: reusable, resets after all threads arrive, all threads wait for each other at the barrier point. Use CountDownLatch for "wait for N events"; CyclicBarrier for "all threads meet at checkpoint, then proceed together".
Q7: How do you implement a thread pool from scratch?
A: Create a BlockingQueue for tasks, create N worker threads that loop calling queue.take() and execute the Runnable. The thread pool class offers submit(Runnable) which calls queue.put(). Add shutdown logic (poison pills or interrupt), rejected execution handling, and optionally a results Future mechanism.
Q8: What is the Fork/Join framework?
A: Fork/Join (Java 7+) is designed for recursive divide-and-conquer parallelism. Tasks extend RecursiveTask<V> or RecursiveAction. Large tasks fork() subtasks, then join() results. Uses work-stealing — idle threads steal tasks from busy threads' queues. Ideal for: merge sort, parallel array operations, recursive computations.
Q9: How do you handle exceptions across multiple threads?
A: (1) Thread.setUncaughtExceptionHandler() for unexpected exceptions. (2) With ExecutorService, future.get() wraps exceptions in ExecutionException. (3) CompletableFuture provides exceptionally() and handle(). (4) For thread groups, set a group-level handler. Always log exceptions — silent thread death is hard to debug.
Q10: What is a Semaphore and when do you use it?
A: Semaphore controls access to a limited number of resources (permits). acquire() gets a permit (blocks if none available), release() returns a permit. Use for: connection pools (limit concurrent connections), rate limiting, bounded resource access. Unlike locks, semaphores can be released by a different thread than the acquirer.