Interview Questions
This comprehensive guide covers common and advanced Java concurrency interview questions, with detailed answers and code examples suitable for senior Java engineers.
Basic Concepts
Section titled “Basic Concepts”1. What is the difference between a process and a thread?
Section titled “1. What is the difference between a process and a thread?”Answer: A process is an independent program with its own memory space, while a thread is a lightweight execution unit within a process.
Key differences:
- Memory: Processes have separate memory spaces; threads share the same memory space within a process
- Communication: Inter-process communication is more complex than inter-thread communication
- Creation/Termination: Creating/terminating processes is more resource-intensive than threads
- Context switching: Switching between threads is faster than switching between processes
- Isolation: Processes are isolated; a crash in one process doesn’t affect others, while a thread crash may affect the entire process
Example:
// Creating a new processProcessBuilder pb = new ProcessBuilder("java", "-jar", "app.jar");Process process = pb.start();
// Creating a new threadThread thread = new Thread(() -> { System.out.println("Running in a new thread");});thread.start();2. What are the different ways to create a thread in Java?
Section titled “2. What are the different ways to create a thread in Java?”Answer: There are four main ways to create a thread in Java:
1. Extending the Thread class:
class MyThread extends Thread { public void run() { System.out.println("Thread running: " + Thread.currentThread().getName()); }}
// UsageMyThread thread = new MyThread();thread.start();2. Implementing the Runnable interface:
class MyRunnable implements Runnable { public void run() { System.out.println("Thread running: " + Thread.currentThread().getName()); }}
// UsageThread thread = new Thread(new MyRunnable());thread.start();3. Using lambda expressions (Java 8+):
Thread thread = new Thread(() -> { System.out.println("Thread running: " + Thread.currentThread().getName());});thread.start();4. Using the Executor framework:
ExecutorService executor = Executors.newSingleThreadExecutor();executor.submit(() -> { System.out.println("Thread running: " + Thread.currentThread().getName());});executor.shutdown();Best practice: Implementing Runnable is generally preferred over extending Thread as it:
- Doesn’t waste inheritance
- Allows the task to be executed in different contexts (Thread, ExecutorService, etc.)
- Separates the task (what) from the execution mechanism (how)
3. What is the thread lifecycle in Java?
Section titled “3. What is the thread lifecycle in Java?”Answer: A Java thread goes through the following states during its lifecycle:
- NEW: Thread is created but not yet started
- RUNNABLE: Thread is ready to run and waiting for CPU allocation
- BLOCKED: Thread is waiting to acquire a monitor lock
- WAITING: Thread is waiting indefinitely for another thread to perform a particular action
- TIMED_WAITING: Thread is waiting for another thread for a specified period
- TERMINATED: Thread has completed execution or was stopped
Code example to demonstrate thread states:
Thread thread = new Thread(() -> { try { // Thread moves to TIMED_WAITING state Thread.sleep(1000);
synchronized (this) { // Thread might move to BLOCKED state if lock is not available // Once lock is acquired, thread is RUNNABLE }
// Thread moves to WAITING state Object lock = new Object(); synchronized (lock) { lock.wait(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); }});
// Thread is in NEW stateSystem.out.println("State: " + thread.getState()); // NEW
thread.start();// Thread is now RUNNABLESystem.out.println("State: " + thread.getState()); // RUNNABLE
Thread.sleep(1500);// Thread might be in TIMED_WAITING stateSystem.out.println("State: " + thread.getState()); // TIMED_WAITING
// Eventually thread will be TERMINATEDthread.join();System.out.println("State: " + thread.getState()); // TERMINATED4. What is thread safety? Why is it important?
Section titled “4. What is thread safety? Why is it important?”Answer: Thread safety refers to the property of code that ensures it functions correctly during simultaneous execution by multiple threads. A class or method is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the threads by the runtime environment.
Importance:
- Prevents data corruption and race conditions
- Ensures program correctness in concurrent environments
- Avoids hard-to-debug issues that may appear intermittently
- Critical for applications that handle multiple users or tasks simultaneously
Thread safety can be achieved through:
- Immutability: Using immutable objects
- Synchronization: Using
synchronizedkeyword or locks - Atomic operations: Using atomic classes like
AtomicInteger - Thread confinement: Restricting access to data to a single thread
- Thread-local storage: Using
ThreadLocalvariables - Concurrent collections: Using thread-safe collections
Example of thread-safe vs. non-thread-safe code:
// Not thread-safeclass Counter { private int count = 0;
public void increment() { count++; // Not atomic operation }
public int getCount() { return count; }}
// Thread-safe versionclass ThreadSafeCounter { private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); // Atomic operation }
public int getCount() { return count.get(); }}5. What is the difference between start() and run() methods in Thread?
Section titled “5. What is the difference between start() and run() methods in Thread?”Answer:
start(): Creates a new thread and causes this thread to begin execution. The JVM calls therun()method of this thread.run(): Contains the code that constitutes the new thread. If called directly, it runs in the current thread.
Key differences:
- Thread creation:
start()creates a new thread;run()does not - Execution context:
start()executes in a new thread;run()executes in the current thread - Multiple invocations:
start()can be called only once per Thread object;run()can be called multiple times
Example:
Thread thread = new Thread(() -> { System.out.println("Current thread: " + Thread.currentThread().getName());});
// Creates and starts a new threadthread.start();// Output: Current thread: Thread-0
// Runs in the current thread (main)thread.run();// Output: Current thread: main
// Calling start() again throws IllegalThreadStateException// thread.start(); // ErrorThreads and Synchronization
Section titled “Threads and Synchronization”6. What is synchronization in Java? Why is it needed?
Section titled “6. What is synchronization in Java? Why is it needed?”Answer:
Synchronization in Java is a mechanism that ensures that only one thread can access a resource at a time. It’s implemented using the synchronized keyword, which can be applied to methods or blocks of code.
Why it’s needed:
- Thread safety: Prevents data corruption when multiple threads access shared resources
- Visibility: Ensures changes made by one thread are visible to other threads
- Ordering: Establishes happens-before relationships between threads
- Atomicity: Ensures that operations are completed as a single, indivisible unit
Types of synchronization:
- Method synchronization: Locks the entire method
- Block synchronization: Locks only a specific block of code
- Static synchronization: Locks on the class object
Example:
class Counter { private int count = 0;
// Method synchronization public synchronized void increment() { count++; }
// Block synchronization public void incrementWithBlock() { synchronized(this) { count++; } // Other non-synchronized code }
// Static synchronization public static synchronized void staticMethod() { // Synchronized on Counter.class }
public int getCount() { synchronized(this) { return count; } }}Important notes:
- Synchronization introduces overhead and can reduce performance
- Excessive synchronization can lead to deadlocks
- Synchronization should be used judiciously and only when necessary
7. What is the difference between synchronized method and synchronized block?
Section titled “7. What is the difference between synchronized method and synchronized block?”Answer: Both synchronized methods and blocks use intrinsic locks to ensure thread safety, but they differ in scope and flexibility.
Synchronized Method:
public synchronized void method() { // Entire method is synchronized on 'this'}
public static synchronized void staticMethod() { // Synchronized on the Class object}Synchronized Block:
public void method() { // Non-synchronized code synchronized(this) { // Synchronized code } // More non-synchronized code}
public void methodWithDifferentLock() { Object lock = new Object(); synchronized(lock) { // Synchronized on the lock object }}Key differences:
- Granularity: Blocks provide finer-grained control over what code is synchronized
- Lock object: Methods always lock on
this(or the Class object for static methods), while blocks can lock on any object - Performance: Blocks can be more efficient by minimizing the synchronized code
- Flexibility: Blocks allow using different lock objects for different parts of the code
Best practices:
- Use synchronized blocks instead of methods when possible
- Keep synchronized blocks as small as possible
- Avoid synchronizing on publicly accessible objects
- Consider using explicit locks (
ReentrantLock, etc.) for more advanced scenarios
8. What is the volatile keyword in Java?
Section titled “8. What is the volatile keyword in Java?”Answer:
The volatile keyword in Java is used to indicate that a variable’s value may be modified by different threads simultaneously. It provides two key guarantees:
- Visibility: Changes made to a volatile variable by one thread are immediately visible to all other threads
- Ordering: Prevents instruction reordering optimizations around volatile accesses
What volatile does NOT provide:
- It does not make compound operations (like i++) atomic
- It does not provide mutual exclusion
- It does not create a critical section
Example:
public class SharedFlag { private volatile boolean flag = false;
public void setFlag() { flag = true; // Write is immediately visible to other threads }
public boolean isSet() { return flag; // Always reads the most recent value }
// Incorrect usage - not atomic private volatile int counter = 0; public void increment() { counter++; // Not atomic despite volatile }}Appropriate uses:
- Status flags that are read by multiple threads
- Double-checked locking pattern (Java 5+)
- Publishing immutable objects without synchronization
Example of double-checked locking:
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; }}9. What is a race condition? How can it be prevented?
Section titled “9. What is a race condition? How can it be prevented?”Answer: A race condition occurs when the behavior of a program depends on the relative timing or interleaving of multiple threads or processes. It happens when threads operate on shared data concurrently, and the final outcome depends on the order of execution.
Example of a race condition:
class Counter { private int count = 0;
public void increment() { count++; // Read-modify-write operation is not atomic }
public int getCount() { return count; }}
// Usage that causes race conditionCounter 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();System.out.println(counter.getCount()); // May not be 2000Prevention techniques:
-
Synchronization: Use
synchronizedmethods or blockspublic synchronized void increment() {count++;} -
Atomic variables: Use atomic classes from
java.util.concurrent.atomicprivate AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();} -
Locks: Use explicit locks from
java.util.concurrent.locksprivate final Lock lock = new ReentrantLock();public void increment() {lock.lock();try {count++;} finally {lock.unlock();}} -
Thread confinement: Restrict access to data to a single thread
ThreadLocal<Integer> localCounter = ThreadLocal.withInitial(() -> 0); -
Immutable objects: Use immutable data structures that can’t be modified after creation
-
Non-blocking algorithms: Use compare-and-swap (CAS) operations
public void incrementWithCAS() {int current;int next;do {current = count;next = current + 1;} while (!atomicCount.compareAndSet(current, next));}
10. What is a deadlock? How can it be prevented?
Section titled “10. What is a deadlock? How can it be prevented?”Answer: A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a lock. Deadlocks typically occur when multiple threads need the same locks but obtain them in different orders.
Classic deadlock example:
Object lock1 = new Object();Object lock2 = new Object();
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: Holding lock 1 & 2..."); } }});
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: Holding lock 1 & 2..."); } }});
t1.start();t2.start();Prevention techniques:
-
Lock ordering: Always acquire locks in a fixed, global order
// Both threads use the same lock ordersynchronized(lock1) {synchronized(lock2) {// Work with both resources}} -
Lock timeouts: Use timed lock attempts
Lock lock1 = new ReentrantLock();Lock lock2 = new ReentrantLock();boolean acquired = false;try {acquired = lock1.tryLock(1, TimeUnit.SECONDS);if (acquired) {try {acquired = lock2.tryLock(1, TimeUnit.SECONDS);if (acquired) {// Work with both locks}} finally {if (acquired) lock2.unlock();}}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {if (acquired) lock1.unlock();} -
Deadlock detection: Use thread dumps or management APIs to detect and recover from deadlocks
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();if (deadlockedThreads != null) {// Handle deadlock} -
Lock hierarchy: Design a proper lock hierarchy and document it
-
Avoid nested locks: Minimize the use of nested locks when possible
-
Use higher-level concurrency utilities: Use concurrent collections, atomic variables, etc., which are designed to avoid deadlocks
Locks and Atomic Variables
Section titled “Locks and Atomic Variables”11. What is the difference between synchronized and ReentrantLock?
Section titled “11. What is the difference between synchronized and ReentrantLock?”Answer:
synchronized is a built-in language feature for locking, while ReentrantLock is a class in the java.util.concurrent.locks package that offers more advanced features.
Key differences:
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Syntax | Language construct | API-based |
| Timed lock acquisition | No | Yes (tryLock(time)) |
| Interruptible locking | No | Yes (lockInterruptibly()) |
| Non-blocking attempts | No | Yes (tryLock()) |
| Fairness policy | No | Yes (optional constructor parameter) |
| Multiple conditions | No | Yes (via newCondition()) |
| Lock state inspection | No | Yes (methods like isHeldByCurrentThread()) |
| Explicit unlocking | Automatic | Manual (must call unlock() in finally block) |
Example of ReentrantLock:
private final ReentrantLock lock = new ReentrantLock(true); // Fair lock
public void method() { lock.lock(); try { // Critical section } finally { lock.unlock(); // Must be in finally block }}
public void methodWithTimeout() throws InterruptedException { if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // Critical section } finally { lock.unlock(); } } else { // Could not acquire lock within timeout }}Best practices:
- Use
synchronizedfor simple locking needs - Use
ReentrantLockwhen you need advanced features - Always release locks in a finally block
- Consider fairness requirements
12. What is a ReadWriteLock? When would you use it?
Section titled “12. What is a ReadWriteLock? When would you use it?”Answer:
A ReadWriteLock maintains a pair of locks: one for read-only operations and one for write operations. Multiple threads can hold the read lock simultaneously, but the write lock is exclusive.
Key characteristics:
- Multiple readers can access simultaneously
- Writers have exclusive access
- Writers are usually prioritized over readers
Example:
public class ThreadSafeCache { private final Map<String, Object> cache = new HashMap<>(); private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock();
public Object get(String key) { readLock.lock(); try { return cache.get(key); } finally { readLock.unlock(); } }
public void put(String key, Object value) { writeLock.lock(); try { cache.put(key, value); } finally { writeLock.unlock(); } }
public boolean containsKey(String key) { readLock.lock(); try { return cache.containsKey(key); } finally { readLock.unlock(); } }
public Object remove(String key) { writeLock.lock(); try { return cache.remove(key); } finally { writeLock.unlock(); } }}When to use it:
- Read-heavy workloads with infrequent updates
- When read operations don’t modify shared data
- When you want to improve concurrency for read operations
Types:
ReentrantReadWriteLock: Standard implementationStampedLock(Java 8+): Provides optimistic reading and better throughput
13. What are atomic variables in Java? How do they work?
Section titled “13. What are atomic variables in Java? How do they work?”Answer:
Atomic variables are classes in the java.util.concurrent.atomic package that support lock-free, thread-safe operations on single variables. They use low-level atomic machine instructions like compare-and-swap (CAS) to ensure atomicity without locking.
Common atomic classes:
AtomicInteger,AtomicLong,AtomicBooleanAtomicReference<V>AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray<V>AtomicStampedReference<V>,AtomicMarkableReference<V>
How they work:
- Read the current value
- Compute a new value based on the current value
- Use CAS to update only if the current value hasn’t changed
- If the value has changed, retry from step 1
Example:
public class AtomicCounter { private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); // Atomic operation }
public void decrement() { count.decrementAndGet(); // Atomic operation }
public int get() { return count.get(); }
public void update() { // CAS loop pattern int current; do { current = count.get(); } while (!count.compareAndSet(current, current + 2)); }}Benefits:
- Better performance than locks for single variables
- Immune to deadlocks and livelocks
- Support for atomic compound actions
Limitations:
- Only work on single variables or fields
- Not suitable for coordinating multiple related operations
- Can suffer from ABA problems (solved by
AtomicStampedReference)
14. What is the ABA problem in concurrent programming?
Section titled “14. What is the ABA problem in concurrent programming?”Answer: The ABA problem occurs in concurrent algorithms, particularly lock-free ones, when a thread reads a value A, another thread changes it to B and then back to A, and the first thread doesn’t detect the change.
Example scenario:
- Thread 1 reads value A
- Thread 1 is paused
- Thread 2 changes value from A to B
- Thread 2 changes value from B back to A
- Thread 1 resumes and sees value A, assuming nothing has changed
- Thread 1 performs an operation that may be incorrect because it missed the intermediate state
Example with a stack:
// Simplified lock-free stack with ABA problemclass LockFreeStack<T> { private AtomicReference<Node<T>> top = new AtomicReference<>(null);
public void push(T item) { Node<T> newHead = new Node<>(item); Node<T> oldHead; do { oldHead = top.get(); newHead.next = oldHead; } while (!top.compareAndSet(oldHead, newHead)); }
public T pop() { Node<T> oldHead; Node<T> newHead; do { oldHead = top.get(); if (oldHead == null) return null; newHead = oldHead.next; } while (!top.compareAndSet(oldHead, newHead)); return oldHead.item; }
private static class Node<T> { final T item; Node<T> next;
Node(T item) { this.item = item; } }}Solutions:
-
AtomicStampedReference: Adds a stamp (version number) that changes with each update
AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(100, 0);int[] stamp = new int[1];int initialValue = asr.get(stamp);int initialStamp = stamp[0];// Update only if value and stamp haven't changedboolean success = asr.compareAndSet(initialValue, newValue, initialStamp, initialStamp + 1); -
AtomicMarkableReference: Simpler version with a boolean mark instead of an integer stamp
-
Hazard pointers: Track which references are currently being accessed
-
Memory reclamation techniques: Ensure that objects aren’t reused too quickly
15. What is a ThreadLocal variable? When would you use it?
Section titled “15. What is a ThreadLocal variable? When would you use it?”Answer:
ThreadLocal provides thread-local variables, which are variables that are local to each thread. Each thread has its own, independently initialized copy of the variable, and changes made by one thread don’t affect other threads.
Example:
public class ThreadLocalExample { // Each thread has its own copy private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) { // Uses the thread's own instance return dateFormat.get().format(date); }
// Proper cleanup in frameworks/containers public void cleanup() { dateFormat.remove(); }}When to use it:
- Thread safety: When you need thread-safe access to non-thread-safe objects (like SimpleDateFormat)
- Per-thread context: Storing user IDs, transaction IDs, or other context for the current thread
- Reducing contention: When sharing would cause high contention
- Performance: Avoiding synchronization overhead for thread-specific data
Important considerations:
- Memory usage increases with the number of threads
- Memory leaks can occur if not properly cleaned up, especially in application servers
- Use
remove()when the thread is done with the variable - Consider using
ThreadLocal.withInitial()(Java 8+) for cleaner initialization
Concurrent Collections
Section titled “Concurrent Collections”16. What are concurrent collections in Java? How are they different from synchronized collections?
Section titled “16. What are concurrent collections in Java? How are they different from synchronized collections?”Answer:
Concurrent collections are thread-safe collections in the java.util.concurrent package designed for high concurrency. Synchronized collections are older thread-safe wrappers created using Collections.synchronizedXxx methods.
Key differences:
| Aspect | Synchronized Collections | Concurrent Collections |
|---|---|---|
| Implementation | Wrapper with synchronized methods | Specialized algorithms (lock striping, CAS) |
| Locking | Single lock for the entire collection | Fine-grained locking or lock-free |
| Iterators | Fail-fast (throw ConcurrentModificationException) | Weakly consistent (may reflect some changes) |
| Performance | Lower throughput under contention | Higher throughput under contention |
| Blocking operations | No built-in support | Some collections offer blocking operations |
Common concurrent collections:
ConcurrentHashMap: High-concurrency map implementationCopyOnWriteArrayList: Thread-safe variant of ArrayList for read-heavy workloadsCopyOnWriteArraySet: Set implementation backed by CopyOnWriteArrayListConcurrentSkipListMap: Concurrent NavigableMap implementationConcurrentSkipListSet: Concurrent NavigableSet implementationConcurrentLinkedQueue: Non-blocking queueConcurrentLinkedDeque: Non-blocking double-ended queueBlockingQueueimplementations:LinkedBlockingQueue,ArrayBlockingQueue, etc.
Example:
// Synchronized collectionMap<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());// Must manually synchronize for compound operationssynchronized (syncMap) { if (!syncMap.containsKey("key")) { syncMap.put("key", "value"); }}
// Concurrent collectionConcurrentMap<String, String> concMap = new ConcurrentHashMap<>();// Atomic compound operations built-inconcMap.putIfAbsent("key", "value");
// Iteration differsList<String> syncList = Collections.synchronizedList(new ArrayList<>());synchronized (syncList) { // Must synchronize iteration for (String item : syncList) { // Safe iteration }}
List<String> concList = new CopyOnWriteArrayList<>();for (String item : concList) { // No synchronization needed // Safe iteration, but may not reflect concurrent modifications}Best practices:
- Prefer concurrent collections over synchronized collections
- Choose the right collection for your access pattern
- Be aware of the iterator consistency guarantees
- Use the built-in atomic operations when available
17. How does ConcurrentHashMap work internally? How is it different from HashMap and Hashtable?
Section titled “17. How does ConcurrentHashMap work internally? How is it different from HashMap and Hashtable?”Answer:
ConcurrentHashMap is a thread-safe hash table designed for high concurrency. It achieves this through techniques like lock striping (segmentation) in earlier versions and a more sophisticated approach in Java 8+.
Internal working:
- Java 7 and earlier: Uses segments (essentially mini-hashtables) with a separate lock per segment
- Java 8+: Uses a combination of CAS operations for updates and synchronized blocks on individual hash table bins
Key differences:
| Feature | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| Thread safety | Not thread-safe | Thread-safe | Thread-safe |
| Locking mechanism | None | Single lock on entire table | Fine-grained locking |
| Null keys/values | Allows null keys and values | Doesn’t allow null keys or values | Doesn’t allow null keys or values |
| Iterator behavior | Fail-fast | Fail-fast | Weakly consistent |
| Performance under contention | N/A (not thread-safe) | Poor (single lock) | Good (fine-grained locking) |
| Atomic compound operations | None | None | putIfAbsent, replace, etc. |
Example:
// HashMap - not thread-safeMap<String, Integer> hashMap = new HashMap<>();
// Hashtable - thread-safe but poor concurrencyMap<String, Integer> hashtable = new Hashtable<>();
// ConcurrentHashMap - thread-safe with good concurrencyConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// Atomic operations in ConcurrentHashMapconcurrentMap.putIfAbsent("key", 1);concurrentMap.replace("key", 1, 2);concurrentMap.remove("key", 2);
// Bulk operationsconcurrentMap.forEach(8, (k, v) -> System.out.println(k + ": " + v));Performance considerations:
ConcurrentHashMaphas slightly higher overhead for uncontended accessConcurrentHashMapperforms much better under contention- The default concurrency level is automatically determined based on the number of CPUs
18. What are blocking queues in Java? Give examples of different types.
Section titled “18. What are blocking queues in Java? Give examples of different types.”Answer:
Blocking queues are thread-safe queues that support operations that wait (block) for the queue to become non-empty when retrieving elements, or wait for space to become available when adding elements. They are part of the java.util.concurrent package and implement the BlockingQueue interface.
Key operations:
- put(e): Adds an element, waiting if necessary for space to become available
- take(): Retrieves and removes an element, waiting if necessary for an element to become available
- offer(e, time, unit): Adds an element, waiting up to the specified time if necessary
- poll(time, unit): Retrieves and removes an element, waiting up to the specified time if necessary
Types of blocking queues:
-
ArrayBlockingQueue: Bounded queue backed by an array
// Fixed capacity, optional fairness policyBlockingQueue<String> queue = new ArrayBlockingQueue<>(100, true); -
LinkedBlockingQueue: Optionally bounded queue backed by linked nodes
// UnboundedBlockingQueue<String> unbounded = new LinkedBlockingQueue<>();// BoundedBlockingQueue<String> bounded = new LinkedBlockingQueue<>(100); -
PriorityBlockingQueue: Unbounded priority queue
// Elements dequeued according to their natural orderBlockingQueue<Integer> priorityQueue = new PriorityBlockingQueue<>();// Or with a custom comparatorBlockingQueue<Task> taskQueue = new PriorityBlockingQueue<>(11,Comparator.comparing(Task::getPriority)); -
DelayQueue: Queue where elements can only be taken when their delay has expired
BlockingQueue<DelayedTask> delayQueue = new DelayQueue<>();// Elements must implement Delayed interfacedelayQueue.put(new DelayedTask("Task", 5, TimeUnit.SECONDS)); -
SynchronousQueue: Queue with no internal capacity
// Each put must wait for a take, and vice versaBlockingQueue<String> syncQueue = new SynchronousQueue<>(); -
LinkedTransferQueue: Unbounded queue that allows producers to wait for consumers
TransferQueue<String> transferQueue = new LinkedTransferQueue<>();// Normal operations plus transfer methodstransferQueue.transfer("item"); // Waits until received by a consumer -
LinkedBlockingDeque: Deque version of LinkedBlockingQueue
BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>();// Supports operations at both endsblockingDeque.putFirst("first");blockingDeque.putLast("last");
Example: Producer-Consumer pattern
class Producer implements Runnable { private final BlockingQueue<String> queue;
Producer(BlockingQueue<String> queue) { this.queue = queue; }
@Override public void run() { try { for (int i = 0; i < 100; i++) { String item = "Item " + i; queue.put(item); // Blocks if queue is full System.out.println("Produced: " + item); Thread.sleep(100); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}
class Consumer implements Runnable { private final BlockingQueue<String> queue;
Consumer(BlockingQueue<String> queue) { this.queue = queue; }
@Override public void run() { try { while (true) { String item = queue.take(); // Blocks if queue is empty System.out.println("Consumed: " + item); Thread.sleep(200); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}
// UsageBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);new Thread(new Producer(queue)).start();new Thread(new Consumer(queue)).start();19. What is the difference between CopyOnWriteArrayList and ArrayList?
Section titled “19. What is the difference between CopyOnWriteArrayList and ArrayList?”Answer:
CopyOnWriteArrayList is a thread-safe variant of ArrayList in which all mutative operations (add, set, remove, etc.) are implemented by creating a fresh copy of the underlying array. It’s designed for cases where reads vastly outnumber writes.
Key differences:
| Feature | ArrayList | CopyOnWriteArrayList |
|---|---|---|
| Thread safety | Not thread-safe | Thread-safe |
| Implementation | Backed by a resizable array | Creates a new array copy for each modification |
| Iterator behavior | Fail-fast | Snapshot view (never throws ConcurrentModificationException) |
| Performance for reads | Good | Good |
| Performance for writes | Good | Poor (creates a new array copy) |
| Memory usage | Efficient | Higher (due to copying) |
Example:
// ArrayList - not thread-safeList<String> arrayList = new ArrayList<>();
// CopyOnWriteArrayList - thread-safeList<String> cowList = new CopyOnWriteArrayList<>();
// Adding elementscowList.add("A"); // Creates a new arraycowList.add("B"); // Creates another new array
// Safe iteration without external synchronizationfor (String s : cowList) { System.out.println(s);}
// Iterator doesn't support modificationIterator<String> it = cowList.iterator();while (it.hasNext()) { String s = it.next(); // it.remove(); // Throws UnsupportedOperationException}When to use CopyOnWriteArrayList:
- Read-heavy, write-rare scenarios
- When you need thread-safe iteration without synchronization
- When you need to prevent ConcurrentModificationException
- For event listener lists that are rarely modified but often iterated
When to avoid CopyOnWriteArrayList:
- Write-heavy scenarios
- Large lists with frequent modifications
- When memory usage is a concern
20. What are concurrent synchronizers in Java? Explain CountDownLatch, CyclicBarrier, and Semaphore.
Section titled “20. What are concurrent synchronizers in Java? Explain CountDownLatch, CyclicBarrier, and Semaphore.”Answer:
Concurrent synchronizers are utility classes in the java.util.concurrent package that facilitate common forms of synchronization between threads.
1. CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
Key characteristics:
- Initialized with a count
countDown()decrements the countawait()blocks until count reaches zero- Cannot be reset once count reaches zero
Example:
public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { int workerCount = 5; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(workerCount);
for (int i = 0; i < workerCount; i++) { final int workerId = i; new Thread(() -> { try { startSignal.await(); // Wait for start signal System.out.println("Worker " + workerId + " started"); // Do work Thread.sleep((long) (Math.random() * 1000)); System.out.println("Worker " + workerId + " finished"); doneSignal.countDown(); // Signal completion } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); }
// Start all workers simultaneously System.out.println("Starting all workers"); startSignal.countDown();
// Wait for all workers to finish doneSignal.await(); System.out.println("All workers finished"); }}Use cases:
- Starting a group of threads simultaneously
- Waiting for a group of threads to complete
- Implementing a simple one-time gate
2. CyclicBarrier: A synchronization aid that allows a set of threads to wait for each other to reach a common barrier point.
Key characteristics:
- Initialized with a party count (and optionally a runnable)
await()blocks until all parties have called await()- Can be reused after all parties have reached the barrier
- Optional barrier action runs when barrier is tripped
Example:
public class CyclicBarrierExample { public static void main(String[] args) { int parties = 3; int iterations = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> { // This runs when all threads reach the barrier System.out.println("All parties have reached the barrier!"); });
for (int i = 0; i < parties; i++) { final int threadId = i; new Thread(() -> { try { for (int j = 0; j < iterations; j++) { System.out.println("Thread " + threadId + " preparing for iteration " + j); Thread.sleep((long) (Math.random() * 1000)); System.out.println("Thread " + threadId + " waiting at barrier"); barrier.await(); // Wait for all parties System.out.println("Thread " + threadId + " crossed the barrier"); } } catch (InterruptedException | BrokenBarrierException e) { Thread.currentThread().interrupt(); } }).start(); } }}Use cases:
- Parallel computations where threads need to synchronize at certain points
- Simulations where multiple entities need to be ready before proceeding
- Multi-phase computations
3. Semaphore: A synchronization aid that controls access to a shared resource through the use of permits.
Key characteristics:
- Initialized with a number of permits
acquire()obtains a permit, blocking if none availablerelease()returns a permit to the semaphore- Can be fair or unfair (default is unfair)
Example:
public class SemaphoreExample { public static void main(String[] args) { // Simulate a pool of 3 connections int maxConnections = 3; Semaphore semaphore = new Semaphore(maxConnections, true); // Fair semaphore
for (int i = 0; i < 10; i++) { final int userId = i; new Thread(() -> { try { System.out.println("User " + userId + " is waiting for a connection"); semaphore.acquire(); // Get permit System.out.println("User " + userId + " acquired a connection");
// Simulate using the connection Thread.sleep((long) (Math.random() * 2000));
System.out.println("User " + userId + " releasing connection"); semaphore.release(); // Release permit } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); } }}Use cases:
- Limiting concurrent access to a resource
- Implementing bounded collections
- Implementing producer-consumer with bounded buffer
- Rate limiting
Other synchronizers:
- Phaser: More flexible than CyclicBarrier and CountDownLatch, with dynamic party registration
- Exchanger: Allows two threads to exchange objects at a synchronization point
- StampedLock (Java 8+): A capability-based lock with optimistic reading
Executors and Thread Pools
Section titled “Executors and Thread Pools”21. What is the Executor framework in Java? What are its advantages over directly using threads?
Section titled “21. What is the Executor framework in Java? What are its advantages over directly using threads?”Answer: The Executor framework is a high-level API for launching and managing threads, introduced in Java 5. It separates thread creation and management from the rest of the application logic.
Key interfaces:
- Executor: Simple interface with a single
execute(Runnable)method - ExecutorService: Extended interface with lifecycle and task submission methods
- ScheduledExecutorService: Adds scheduling capabilities
Advantages over direct thread usage:
- Thread pooling: Reuses threads to reduce the overhead of thread creation
- Task queuing: Manages tasks when all threads are busy
- Lifecycle management: Provides methods to shut down gracefully
- Task submission: Supports both Runnable and Callable tasks
- Future results: Returns Future objects for tracking task completion
- Scheduling: Supports delayed and periodic task execution
- Thread factory: Allows customization of thread creation
- Rejection policies: Controls behavior when the executor is saturated
Example:
// Simple thread creationfor (int i = 0; i < 100; i++) { Thread thread = new Thread(() -> { // Task logic }); thread.start();}
// Using Executor frameworkExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++) { executor.submit(() -> { // Task logic });}executor.shutdown();Best practices:
- Always shut down executors explicitly
- Use appropriate pool sizes based on workload characteristics
- Consider using different executors for different types of tasks
- Handle rejected executions appropriately
22. What are the different types of thread pools in Java? When would you use each?
Section titled “22. What are the different types of thread pools in Java? When would you use each?”Answer:
Java provides several pre-configured thread pool implementations through factory methods in the Executors class.
1. Fixed Thread Pool:
ExecutorService fixedPool = Executors.newFixedThreadPool(nThreads);- Creates a pool with a fixed number of threads
- If all threads are busy, new tasks wait in an unbounded queue
- Good for limiting resource usage and when you know the optimal thread count
2. Cached Thread Pool:
ExecutorService cachedPool = Executors.newCachedThreadPool();- Creates new threads as needed, reuses idle threads
- Threads that remain idle for 60 seconds are terminated
- Good for many short-lived tasks and when demand varies
3. Single Thread Executor:
ExecutorService singlePool = Executors.newSingleThreadExecutor();- Uses a single worker thread with an unbounded queue
- Guarantees sequential execution of tasks
- Good for tasks that must run sequentially
4. Scheduled Thread Pool:
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(corePoolSize);- Fixed-size pool that supports delayed and periodic task execution
- Good for recurring tasks or tasks that need to start after a delay
5. Work-Stealing Pool (Java 8+):
ExecutorService workStealingPool = Executors.newWorkStealingPool();- Uses a ForkJoinPool with parallelism level equal to available processors
- Employs work-stealing algorithm where idle threads steal tasks from busy threads
- Good for computational tasks that can be broken down recursively
6. Custom ThreadPoolExecutor:
ThreadPoolExecutor customPool = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedExecutionHandler);- Fully customizable thread pool
- Good when you need precise control over pool behavior
When to use each:
- Fixed: When you want to limit resource usage and have a stable number of threads
- Cached: When you have many short-lived tasks and variable load
- Single: When tasks must execute sequentially
- Scheduled: When you need to run tasks on a schedule
- Work-Stealing: For compute-intensive tasks that can be broken down
- Custom: When you need fine-grained control over pool parameters
Warning about unbounded queues:
Both newFixedThreadPool and newSingleThreadExecutor use unbounded queues which can lead to OutOfMemoryError if tasks are submitted faster than they can be processed.
23. How do you properly shut down an ExecutorService?
Section titled “23. How do you properly shut down an ExecutorService?”Answer:
Properly shutting down an ExecutorService involves rejecting new tasks, allowing already-submitted tasks to complete, and potentially interrupting tasks that are taking too long.
Basic shutdown:
executor.shutdown(); // Rejects new tasks, allows existing tasks to finishComplete shutdown pattern:
ExecutorService executor = Executors.newFixedThreadPool(10);try { // Submit tasks executor.submit(() -> { /* task */ });} finally { // Initiate orderly shutdown executor.shutdown(); try { // Wait for existing tasks to terminate if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // Cancel currently executing tasks forcefully executor.shutdownNow(); // Wait for tasks to respond to being cancelled if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println("Executor did not terminate"); } } } catch (InterruptedException e) { // (Re-)Cancel if current thread also interrupted executor.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); }}Key methods:
- shutdown(): Initiates orderly shutdown, rejects new tasks but executes already submitted tasks
- shutdownNow(): Attempts to stop all actively executing tasks and returns a list of tasks that were awaiting execution
- awaitTermination(long timeout, TimeUnit unit): Blocks until all tasks have completed, the timeout occurs, or the current thread is interrupted
- isShutdown(): Returns true if shutdown has been initiated
- isTerminated(): Returns true if all tasks have completed following shutdown
Best practices:
- Always call
shutdown()when you’re done with an executor - Use try-finally to ensure shutdown happens even if exceptions occur
- Consider using a timeout with
awaitTermination()to avoid hanging indefinitely - Preserve the interrupt status if
awaitTermination()is interrupted - In web applications or long-running services, register a shutdown hook to ensure proper executor shutdown
24. What is the Fork/Join framework? How is it different from ExecutorService?
Section titled “24. What is the Fork/Join framework? How is it different from ExecutorService?”Answer: The Fork/Join framework, introduced in Java 7, is a specialized implementation of the ExecutorService designed for tasks that can be broken down into smaller subtasks recursively (divide-and-conquer algorithm).
Key components:
- ForkJoinPool: Specialized thread pool that uses work-stealing algorithm
- ForkJoinTask: Abstract base class for tasks that run in a ForkJoinPool
- RecursiveTask
: ForkJoinTask that returns a result - RecursiveAction: ForkJoinTask that doesn’t return a result
How it works:
- A task is split into smaller subtasks (fork)
- Each subtask is executed in parallel
- Results of subtasks are combined (join)
- Idle worker threads steal tasks from busy threads’ queues
Example: Computing Fibonacci numbers:
public class FibonacciTask extends RecursiveTask<Integer> { private final int n; private static final int THRESHOLD = 10;
public FibonacciTask(int n) { this.n = n; }
@Override protected Integer compute() { if (n <= THRESHOLD) { // Base case: compute directly return computeDirectly(); }
// Split into subtasks FibonacciTask f1 = new FibonacciTask(n - 1); f1.fork(); // Submit subtask
FibonacciTask f2 = new FibonacciTask(n - 2); int result = f2.compute() + f1.join(); // Compute f2 and wait for f1
return result; }
private int computeDirectly() { if (n <= 1) return n; int a = 0, b = 1; for (int i = 2; i <= n; i++) { int c = a + b; a = b; b = c; } return b; }}
// UsageForkJoinPool pool = new ForkJoinPool();int result = pool.invoke(new FibonacciTask(30));Differences from standard ExecutorService:
| Feature | Standard ExecutorService | Fork/Join Framework |
|---|---|---|
| Task type | Independent tasks | Recursive, divisible tasks |
| Work distribution | Tasks submitted externally | Tasks create subtasks |
| Load balancing | Fixed assignment | Work stealing |
| Thread management | Typically fixed number | Automatically adapts |
| Task coordination | Via Future.get() | Via ForkJoinTask.join() |
| Blocking behavior | Blocks calling thread | Can use ManagedBlocker |
When to use Fork/Join:
- Compute-intensive tasks (not I/O-bound tasks)
- Tasks that can be divided into smaller subtasks
- When you want to maximize CPU utilization
- Examples: sorting large arrays, matrix multiplication, image processing
Best practices:
- Make the base case threshold large enough to amortize the overhead
- Avoid blocking operations in tasks
- Use
invokeAll()for multiple subtasks - Consider using Java 8+ parallel streams which use Fork/Join internally
CompletableFuture and Asynchronous Programming
Section titled “CompletableFuture and Asynchronous Programming”25. What is the difference between Future and CompletableFuture?
Section titled “25. What is the difference between Future and CompletableFuture?”Answer:
Future (introduced in Java 5) represents the result of an asynchronous computation, while CompletableFuture (introduced in Java 8) extends Future with a rich set of methods for composing, combining, and handling asynchronous operations.
Key differences:
| Feature | Future | CompletableFuture |
|---|---|---|
| Completion notification | Must poll with isDone() or block with get() | Can register callbacks with thenApply(), thenAccept(), etc. |
| Composition | Not supported | Supports chaining with thenCompose() |
| Combination | Not supported | Can combine multiple futures with thenCombine(), allOf(), etc. |
| Exception handling | Must catch exceptions from get() | Provides exceptionally(), handle(), etc. |
| Manual completion | Not supported | Can complete manually with complete(), completeExceptionally() |
| Cancellation | Basic cancel() method | Enhanced with completeExceptionally() and cancel() |
Example: Future vs CompletableFuture
// Using FutureExecutorService executor = Executors.newFixedThreadPool(1);Future<String> future = executor.submit(() -> { Thread.sleep(1000); return "Result";});
// Blocking calltry { String result = future.get(); // Blocks until completed System.out.println(result);} catch (InterruptedException | ExecutionException e) { e.printStackTrace();}executor.shutdown();
// Using CompletableFutureCompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); return "Result"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Error"; }});
// Non-blocking with callbackscf.thenAccept(result -> System.out.println(result));
// Or transform the resultCompletableFuture<Integer> lengthFuture = cf.thenApply(String::length);lengthFuture.thenAccept(length -> System.out.println("Length: " + length));Benefits of CompletableFuture:
- Non-blocking programming model
- Rich API for composition and combination
- Better exception handling
- Can be completed manually
- Works well with functional programming style
26. How do you handle exceptions in CompletableFuture?
Section titled “26. How do you handle exceptions in CompletableFuture?”Answer: CompletableFuture provides several methods for handling exceptions in asynchronous computations.
1. Using exceptionally():
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { if (Math.random() < 0.5) { throw new RuntimeException("Error occurred"); } return "Success";}).exceptionally(ex -> { System.err.println("Exception: " + ex.getMessage()); return "Default value after error"; // Recovery value});2. Using handle():
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { if (Math.random() < 0.5) { throw new RuntimeException("Error occurred"); } return "Success";}).handle((result, ex) -> { if (ex != null) { System.err.println("Exception: " + ex.getMessage()); return "Default value after error"; } return result;});3. Using whenComplete():
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { if (Math.random() < 0.5) { throw new RuntimeException("Error occurred"); } return "Success";}).whenComplete((result, ex) -> { if (ex != null) { System.err.println("Exception occurred: " + ex.getMessage()); }});// Note: whenComplete doesn't change the result or error4. Manually completing with exception:
CompletableFuture<String> future = new CompletableFuture<>();try { // Some operation future.complete("Result");} catch (Exception e) { future.completeExceptionally(e);}Key points:
exceptionally(): Handles exceptions and returns a recovery valuehandle(): Handles both normal result and exceptionwhenComplete(): Performs an action when the future completes (normally or exceptionally)completeExceptionally(): Completes the future with an exception
Best practices:
- Place exception handlers as close as possible to the source of exceptions
- Use
exceptionally()for simple recovery - Use
handle()when you need to process both success and failure cases - Use
whenComplete()for logging or monitoring without changing the result - Consider using
CompletableFuture.exceptionallyCompose()(Java 12+) for recovery with another asynchronous operation
27. How do you combine multiple CompletableFutures?
Section titled “27. How do you combine multiple CompletableFutures?”Answer: CompletableFuture provides several methods for combining multiple asynchronous operations.
1. Combining two futures sequentially (thenCompose):
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
// Use the result of future1 to create future2CompletableFuture<String> future2 = future1.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " World"));
// Output: Hello Worldfuture2.thenAccept(System.out::println);2. Combining two independent futures (thenCombine):
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
// Combine results when both completeCompletableFuture<String> combined = future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2);
// Output: Hello Worldcombined.thenAccept(System.out::println);3. Waiting for all futures to complete (allOf):
List<String> urls = Arrays.asList("url1", "url2", "url3");
// Create a CompletableFuture for each URLList<CompletableFuture<String>> futures = urls.stream() .map(url -> CompletableFuture.supplyAsync(() -> fetchUrl(url))) .collect(Collectors.toList());
// Wait for all to completeCompletableFuture<Void> allDone = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]));
// Process results when all completeCompletableFuture<List<String>> results = allDone.thenApply(v -> futures.stream() .map(CompletableFuture::join) // Safe after allOf .collect(Collectors.toList()));
results.thenAccept(list -> { list.forEach(System.out::println);});
// Helper methodprivate static String fetchUrl(String url) { // Simulate HTTP request try { Thread.sleep(100 + new Random().nextInt(900)); return "Result from " + url; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Error"; }}4. Completing when any future completes (anyOf):
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(100); return "Result from future1"; } catch (InterruptedException e) { return "Error"; }});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(50); return "Result from future2"; } catch (InterruptedException e) { return "Error"; }});
// Complete when either completesCompletableFuture<Object> anyResult = CompletableFuture.anyOf(future1, future2);
// Output: Result from future2 (completes faster)anyResult.thenAccept(System.out::println);5. Running multiple futures in parallel (runAfterBoth):
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> { // Task 1});
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> { // Task 2});
// Run after both completeCompletableFuture<Void> afterBoth = future1.runAfterBoth(future2, () -> { System.out.println("Both tasks completed");});Key methods:
thenCompose(): Sequential composition (flatMap equivalent)thenCombine(): Parallel composition of two futuresallOf(): Waits for all futures to completeanyOf(): Completes when any future completesrunAfterBoth(): Runs an action after two futures completeapplyToEither(): Applies a function to the result of whichever future completes first
28. How do you implement a timeout with CompletableFuture?
Section titled “28. How do you implement a timeout with CompletableFuture?”Answer: There are several ways to implement timeouts with CompletableFuture.
1. Using orTimeout() (Java 9+):
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); // Long-running task return "Result"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Interrupted"; }}).orTimeout(1, TimeUnit.SECONDS); // Completes exceptionally after timeout
future.whenComplete((result, ex) -> { if (ex != null) { System.out.println("Timed out: " + ex.getMessage()); } else { System.out.println(result); }});2. Using completeOnTimeout() (Java 9+):
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); // Long-running task return "Result"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Interrupted"; }}).completeOnTimeout("Default after timeout", 1, TimeUnit.SECONDS);
future.thenAccept(System.out::println); // Prints "Default after timeout"3. Using a separate timeout future (pre-Java 9):
<T> CompletableFuture<T> withTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) { CompletableFuture<T> timeoutFuture = new CompletableFuture<>();
// Schedule a task to complete the timeoutFuture exceptionally after the timeout ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.schedule(() -> { timeoutFuture.completeExceptionally( new TimeoutException("Timeout after " + timeout + " " + unit)); scheduler.shutdown(); }, timeout, unit);
// Return the future that completes first return future.applyToEither(timeoutFuture, Function.identity());}
// UsageCompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); return "Result"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Interrupted"; }});
CompletableFuture<String> withTimeout = withTimeout(future, 1, TimeUnit.SECONDS);withTimeout.whenComplete((result, ex) -> { if (ex != null) { System.out.println("Error: " + ex.getMessage()); } else { System.out.println(result); }});4. Using a timeout future with anyOf (pre-Java 9):
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(2000); return "Result"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Interrupted"; }});
CompletableFuture<String> timeout = new CompletableFuture<>();ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);scheduler.schedule(() -> { timeout.complete("Timeout"); scheduler.shutdown();}, 1, TimeUnit.SECONDS);
CompletableFuture.anyOf(future, timeout).thenAccept(result -> { if ("Timeout".equals(result)) { System.out.println("Operation timed out"); future.cancel(true); // Cancel the original future } else { System.out.println("Result: " + result); }});Best practices:
- Use
orTimeout()orcompleteOnTimeout()in Java 9+ for simplicity - Always clean up resources (like scheduled executors) when using custom timeout implementations
- Consider cancelling the original future when a timeout occurs
- Be careful with thread interruption when cancelling futures
- Choose appropriate timeout values based on the expected operation duration