Java Notes
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
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Program 2: Producer-Consumer with BlockingQueue
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Program 3: Parallel Array Sum
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Program 4: Thread-Safe Bounded Buffer
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Program 5: Countdown Latch — Parallel Service Startup
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
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
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Program 7: Semaphore — Connection Pool
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Program 8: CompletableFuture Chains (Java 8+)
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Summary: When to Use What
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
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.
Continue the lesson path