Multithreading Interview Preparation
Top 15 Multithreading Questions Asked in Java Interviews If you have limited time before an interview, prioritize these: If you have limited time before an interview, prioritize these:…
Top 15 Multithreading Questions Asked in Java Interviews
If you have limited time before an interview, prioritize these:
If you have limited time before an interview, prioritize these:
- What is a Thread? Process vs Thread
- Ways to create a Thread (
Thread,Runnable,Callable) - Thread lifecycle
start()vsrun()- Synchronization and why it’s needed
synchronizedmethod vs block- Object lock vs Class lock
wait()vssleep()wait(),notify(),notifyAll()volatilevssynchronizedExecutorServiceand Thread PoolsFuturevsCompletableFutureConcurrentHashMapvsHashMapvsHashtable- Deadlock and how to avoid it
- Virtual Threads (Java 21) and Parallel Streams (for senior/product-company interviews)
A. Thread Basics
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A Thread is the smallest unit of execution inside a process. A single process can have multiple threads, and all threads share the same process resources like heap memory, files, and network connections, but each thread has its own stack and program counter.
Java supports multithreading to execute multiple tasks concurrently, improving application responsiveness and CPU utilization.
Example
A web server handles multiple client requests simultaneously by assigning each request to a separate thread.
Interview Tip
“A process contains one or more threads. Threads share memory but execute independently.”
| Process | Thread |
|---|---|
| Independent program | Smallest execution unit inside a process |
| Has separate memory | Shares process memory |
| Heavyweight | Lightweight |
| Communication is expensive (IPC) | Communication is fast (shared memory) |
| Context switching is slower | Context switching is faster |
| One process can have many threads | Cannot exist without a process |
Real-world Example
Suppose Chrome is running.
- Chrome Browser = Process
- One thread renders UI
- One thread plays video
- One thread downloads files
- One thread executes JavaScript
Interview Tip
“Processes are isolated from each other, while threads communicate easily because they share the same memory.”
Multithreading allows multiple tasks to execute concurrently within the same process.
Its main advantages are:
- Better CPU utilization
- Faster execution
- Improved responsiveness
- Efficient resource sharing
- Higher throughput
Real-world Examples
- Web server handling multiple users
- Banking system processing transactions
- Online shopping application
- Video streaming
- Chat applications
Interview Tip
“Without multithreading, one slow task can block the entire application.”
There are two primary ways:
1. Extend Thread class
class MyThread extends Thread {
public void run() {
System.out.println("Thread Running");
}
}
new MyThread().start();
2. Implement Runnable interface (Recommended)
class MyTask implements Runnable {
public void run() {
System.out.println("Task Running");
}
}
Thread t = new Thread(new MyTask());
t.start();
Java 8 Lambda
Thread t = new Thread(() -> {
System.out.println("Running");
});
t.start();
Interview Tip
“In modern Java, we usually implement Runnable because Java doesn’t support multiple inheritance.”
| Thread | Runnable |
|---|---|
| Class | Functional Interface |
| Represents the actual thread | Represents only the task |
| Cannot extend another class | Allows extending another class |
| Tightly coupled | Better separation of task and execution |
| Less reusable | More reusable |
Which one is preferred?
Runnable is preferred because:
- Supports multiple inheritance
- Better object-oriented design
- Can be executed by Thread or ExecutorService
- Task and thread remain independent
Interview Tip
“Thread executes the task, Runnable defines the task.”
Interview Answer
| Runnable | Callable |
|---|---|
| Introduced in Java 1.0 | Introduced in Java 5 |
| run() | call() |
| Returns void | Returns a value |
| Cannot throw checked exception | Can throw checked exception |
| Used with Thread | Used with ExecutorService |
Runnable Example
Runnable task = () -> System.out.println("Running");
Callable Example
Callable<Integer> task = () -> 100;
Future<Integer> future = executor.submit(task);
When to use Callable?
Whenever the task:
- returns a result
- throws checked exceptions
- performs asynchronous computation
Interview Tip
“Runnable is fire-and-forget, while Callable is for tasks that produce a result.”
A Java thread goes through the following states:
NEW
|
start()
|
RUNNABLE
|
Running (chosen by CPU scheduler)
|
-----------------------------
| | |
Waiting Timed Waiting Blocked
| | |
-----------------------------
|
RUNNABLE
|
TERMINATED
Important States
| State | Meaning |
|---|---|
| NEW | Thread object created |
| RUNNABLE | Ready to run or running |
| BLOCKED | Waiting for monitor lock |
| WAITING | Waiting indefinitely |
| TIMED_WAITING | Waiting for specified time |
| TERMINATED | Execution completed |
Interview Tip
“Many people think Running is a separate state, but Java’s
Thread.Stateincludes RUNNABLE, which covers both ready-to-run and actively running.”
start()
- Creates a new thread.
- JVM allocates a new call stack.
- Calls
run()internally. - Executes concurrently.
thread.start();
run()
- Normal method call.
- No new thread is created.
- Executes in the current thread.
thread.run();
Interview Tip
“Calling
run()directly does not create a new thread. Always usestart()to begin concurrent execution.”
| sleep() | yield() |
|---|---|
| Pauses the current thread for a specified time | Hints to the scheduler to let another thread run |
| Static method | Static method |
| Moves thread to TIMED_WAITING | Keeps thread in RUNNABLE |
| Guaranteed delay (at least the specified time) | No guarantee another thread will run |
| Throws InterruptedException | Does not throw checked exceptions |
Example
Thread.sleep(1000);
Thread.yield();
Interview Tip
“
yield()is only a scheduling hint; the JVM may ignore it.”
join() makes the current thread wait until the target thread completes its execution.
Example
Thread t = new Thread(() -> {
System.out.println("Task");
});
t.start();
t.join();
System.out.println("Completed");
Output
Task
Completed
Without join(), "Completed" may print before "Task".
Interview Use Cases
- Wait for background tasks.
- Wait for file download completion.
- Wait for worker threads before continuing.
Interview Tip
“
join()is commonly used when one thread depends on the completion of another.”
interrupt() is a mechanism to request that a thread stop what it is doing. It does not forcibly terminate the thread.
If the thread is blocked in methods like sleep(), wait(), or join(), an InterruptedException is thrown. Otherwise, the thread’s interrupted status is set, and the thread should check it and exit gracefully.
Example
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Do work
}
});
t.start();
t.interrupt();
Interview Tip
“
interrupt()is cooperative. The thread must respond to the interruption; it doesn’t kill the thread automatically.”
| User Thread | Daemon Thread |
|---|---|
| Performs application work | Performs background/support tasks |
| JVM waits for it to finish before exiting | JVM does not wait for it |
| Keeps the application alive | Ends automatically when all user threads finish |
| Example: Main thread | Example: Garbage Collector |
Example
Thread t = new Thread(() -> {
while (true) {
System.out.println("Background");
}
});
t.setDaemon(true);
t.start();
Interview Tip
“A daemon thread supports user threads. Once all user threads finish, the JVM exits even if daemon threads are still running.”
B. Synchronization
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Synchronization is a mechanism that ensures only one thread can access a shared resource or critical section at a time, preventing inconsistent data when multiple threads access or modify the same resource concurrently.
In Java, synchronization is primarily achieved using the synchronized keyword, which acquires an intrinsic (monitor) lock before executing the protected code.
Example
Suppose two threads try to withdraw money from the same bank account simultaneously. Without synchronization, both may read the same balance and withdraw, leading to an incorrect balance. Synchronization ensures one withdrawal completes before the other starts.
Interview Tip
“Synchronization provides mutual exclusion—only one thread can execute the synchronized code for a given lock at a time.”
Synchronization is needed because multiple threads often share the same objects. Without synchronization, concurrent updates can lead to:
- Race conditions
- Data inconsistency
- Lost updates
- Unexpected application behavior
Synchronization ensures that shared data remains consistent and predictable.
Example
Without synchronization:
count++; // Not atomic
Internally this performs:
Read count
Increment
Write count
If two threads execute these steps simultaneously, one increment can be lost.
Interview Tip
“Synchronization is required only when multiple threads access shared mutable data.”
| synchronized Method | synchronized Block |
|---|---|
| Locks the entire method | Locks only a specific block |
| Simpler to write | More flexible |
| Lock duration is longer | Lock duration is shorter |
| Lower concurrency | Better concurrency |
| Entire method becomes critical | Only critical code is protected |
synchronized Method
public synchronized void deposit() {
balance += 100;
}
synchronized Block
public void deposit() {
// Non-critical code
synchronized(this) {
balance += 100;
}
// Non-critical code
}
Which is Better?
Generally, synchronized blocks are preferred because they reduce the locked area, allowing better performance and concurrency.
Interview Tip
“Synchronize only the critical section, not the entire method, whenever possible.”
Java provides two types of intrinsic locks.
Object Lock
- Associated with an object instance.
- Acquired when using:
- synchronized instance methods
- synchronized(this)
- Different objects have different locks.
Example:
public synchronized void display() {
}
or
synchronized(this) {
}
Two different objects can execute this method simultaneously because each has its own lock.
Class Lock
- Associated with the Class object.
- Acquired using static synchronized methods.
- Shared by all object instances.
Example
public static synchronized void display() {
}
or
synchronized(MyClass.class) {
}
Only one thread can execute static synchronized code for that class at a time, regardless of how many objects exist.
Interview Tip
“Object lock protects instance data; class lock protects shared static data.”
No. Constructors cannot be declared as synchronized.
Reason:
- Constructors are used only during object creation.
- A constructor is executed by the thread creating the object.
- No other thread can access the object until construction is complete.
If synchronization is needed during initialization, synchronize the methods called by the constructor rather than the constructor itself.
Interview Tip
“The compiler doesn’t allow
synchronizedon constructors because object construction isn’t shared among threads.”
Yes.
A static synchronized method acquires the class lock, not an object lock.
Example
public static synchronized void updateCounter() {
counter++;
}
Equivalent to:
synchronized(MyClass.class) {
counter++;
}
This is commonly used to protect static variables shared across all instances.
Interview Tip
“Static synchronized methods use the Class object as the lock.”
A class or method is thread-safe if it behaves correctly when accessed simultaneously by multiple threads without causing data inconsistency or corruption.
Thread safety can be achieved using:
- Synchronization
- Immutable objects
- Concurrent collections
- Atomic classes
- Proper locking mechanisms
Example
public synchronized void increment() {
count++;
}
Only one thread can update count at a time.
Interview Tip
“Thread safety means correctness under concurrent access.”
A race condition occurs when multiple threads access and modify shared data simultaneously, and the final result depends on the order in which the threads execute.
Example
Initial value:
count = 5
Thread A
Reads 5
Thread B
Reads 5
Thread A
Writes 6
Thread B
Writes 6
Expected value:
7
Actual value:
6
One update is lost.
How to Prevent It?
- synchronized
- Lock
- AtomicInteger
- Other concurrency utilities
Interview Tip
“Race conditions occur because multiple threads modify shared mutable state without proper synchronization.”
A critical section is the part of the code that accesses or modifies shared resources and therefore must not be executed by more than one thread at the same time.
Only the critical section should be synchronized to maximize concurrency.
Example
public void transfer() {
validateAccount();
synchronized(this) {
balance -= amount;
}
sendNotification();
}
Only the balance update requires synchronization.
Interview Tip
“Keep the critical section as small as possible to reduce lock contention and improve performance.”
No. Synchronization introduces locking overhead and can reduce parallelism. It is used for correctness, not performance. However, synchronizing only the critical section minimizes the impact.
Interview Tip
“Synchronization trades a small performance cost for data consistency.”
It depends on the lock they use.
- Same object → No, one thread at a time.
- Different objects → Yes, each object has its own lock.
- Static synchronized methods → No, they share the class lock.
Interview Tip
“Synchronization blocks concurrent execution only for threads competing for the same lock.”
C. Inter-Thread Communication
This topic is a classic Java interview favorite, especially for 3–8+ years experience. Interviewers expect you to understand how threads coordinate with each other rather than just execute concurrently.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Both wait() and sleep() pause a thread, but they serve different purposes.
wait() | sleep() |
|---|---|
Defined in Object | Defined in Thread |
| Used for inter-thread communication | Used to pause execution for a fixed time |
| Releases the monitor lock | Does not release the lock |
| Must be called inside synchronized block/method | Can be called anywhere |
Wakes up by notify(), notifyAll(), timeout, or interrupt | Wakes up after timeout or interrupt |
Example
synchronized(lock) {
lock.wait();
}
Thread.sleep(1000);
Interview Tip: wait() releases the lock, whereas sleep() keeps holding it.
These methods are used for inter-thread communication, allowing threads to coordinate their execution.
wait()
- Releases the object’s monitor lock.
- Moves the thread to the WAITING state.
- Waits until another thread calls
notify()ornotifyAll().
notify()
- Wakes up one waiting thread.
- The awakened thread must re-acquire the lock before continuing.
notifyAll()
- Wakes up all waiting threads.
- Only one thread acquires the lock immediately; others wait for it.
Interview Tip: notifyAll() is generally safer than notify() because it avoids missed notifications and thread starvation.
These methods operate on an object’s monitor (lock), not on the thread itself.
Every Java object has an intrinsic monitor, so the methods belong to Object.
If they were in Thread, it would be unclear which object’s lock should be released or notified.
Interview Tip: Threads wait on an object’s monitor, not on another thread.
A thread must own the object’s monitor before calling wait().
When wait() is called:
- The thread releases the monitor.
- Enters the WAITING state.
- Later re-acquires the monitor before resuming.
If the thread doesn’t own the monitor, the JVM throws:
IllegalMonitorStateException
Interview Tip: wait() manipulates the monitor lock, so the thread must already own that lock.
The Producer-Consumer problem is a classic synchronization problem.
- Producer adds items to a shared buffer.
- Consumer removes items.
- Producer waits if the buffer is full.
- Consumer waits if the buffer is empty.
Using wait() and notifyAll() ensures proper coordination between producer and consumer.
Real-world Example
An online order queue:
- Customers place orders (Producer).
- Delivery system processes orders (Consumer).
Interview Tip: The Producer-Consumer problem demonstrates practical use of wait() and notifyAll() for thread coordination.
D. Java Memory Model
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
The Java Memory Model (JMM) defines how threads interact through memory.
It specifies:
- How variables are stored in main memory.
- How threads cache variables in their working memory.
- Rules for visibility, ordering, and synchronization.
- The JMM ensures predictable behavior across different CPUs and JVM implementations.
Interview Tip: JMM defines how threads see shared data and guarantees visibility and ordering.
volatile ensures that every read of a variable gets the latest value from main memory, and every write is immediately visible to other threads.
It provides:
- Visibility guarantee ✔
- Ordering guarantee ✔
- Atomicity ✘ (except for single variable reads/writes)
Example
private volatile boolean running = true;
Interview Tip: volatile guarantees visibility, not atomicity.
| volatile | synchronized |
|---|---|
| Guarantees visibility | Guarantees visibility and mutual exclusion |
| No locking | Uses monitor locks |
| Lightweight | Higher overhead due to locking |
| No atomicity for compound operations | Compound operations become atomic within the synchronized block |
| Best for status flags | Best for shared mutable state |
Interview Tip: Use volatile when only visibility is needed. Use synchronized when multiple operations must execute atomically.
Visibility
Visibility means one thread’s updates become visible to other threads.
Example
volatile boolean running;
Atomicity
Atomicity means an operation completes as a single indivisible unit.
Example
count++;
This is not atomic because it performs:
- Read
- Increment
- Write
Interview Tip: volatile fixes visibility issues but not race conditions caused by non-atomic operations.
The Happens-Before relationship is a rule defined by the JMM that guarantees memory visibility and ordering between actions.
Example
Unlock happens-before a subsequent lock on the same monitor.
A write to a volatile variable happens-before every subsequent read of that variable.
Thread.start() happens-before any action in the started thread.
All actions in a thread happen-before another thread successfully returns from join().
Interview Tip: Happens-Before defines when one thread is guaranteed to see another thread’s changes.
E. Lock Framework
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
ReentrantLock is an explicit lock implementation from java.util.concurrent.locks that provides the same mutual exclusion as synchronized but with additional features.
Features include:
Explicit lock/unlock
Fair locking (optional)
tryLock()
Interruptible locking
Multiple Condition objects
Example
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
Interview Tip: Always release a ReentrantLock in a finally block.
synchronized
ReentrantLock
Keyword
Class
Lock released automatically
Must unlock manually
Cannot interrupt while waiting
Supports interruptible lock acquisition
No timeout
Supports tryLock() with timeout
No fairness option
Fair lock available
Simpler
More flexible
Interview Tip: Use synchronized for simple cases and ReentrantLock when advanced locking features are required.
tryLock() attempts to acquire the lock without blocking indefinitely.
If the lock is available:
- Returns true
Otherwise:
- Returns false
- It also supports a timeout version.
Interview Tip: tryLock() helps avoid deadlocks by preventing indefinite waiting.
ReadWriteLock provides two separate locks:
- Read Lock
- Write Lock
- Multiple threads can read simultaneously, but only one thread can write, and no readers are allowed during a write.
- Best suited for read-heavy applications.
Example
Cache systems where reads are much more frequent than updates.
Interview Tip: Use ReadWriteLock when reads greatly outnumber writes.
StampedLock is an advanced lock introduced in Java 8 that supports:
- Read lock
- Write lock
- Optimistic read
- Optimistic reads allow reading without immediately acquiring a traditional read lock, improving performance when writes are infrequent.
- Unlike ReentrantLock, StampedLock is not reentrant.
Interview Tip: “StampedLock is useful for highly read-intensive workloads where optimistic reads can reduce locking overhead.”
F. Atomic Classes
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
AtomicInteger provides thread-safe atomic operations on an integer without using synchronized.
It uses low-level Compare-And-Swap (CAS) operations, making it faster than locking in many scenarios.
Example
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
Interview Tip: AtomicInteger avoids locks while providing atomic updates.
CAS is a hardware-supported atomic operation.
It works as follows:
- Read current value.
- Compare it with the expected value.
- If they match, update the value atomically.
- Otherwise, retry.
- This enables lock-free thread-safe algorithms.
Interview Tip: Atomic classes achieve thread safety using CAS instead of traditional locks.
| AtomicInteger | synchronized |
|---|---|
| Lock-free | Lock-based |
| Uses CAS | Uses monitor locks |
| Better performance for simple atomic operations | Better for complex critical sections |
| Supports atomic variable operations | Supports arbitrary synchronized code |
Interview Tip: Use AtomicInteger for single-variable atomic updates and synchronized for protecting larger critical sections.
| AtomicLong | LongAdder |
|---|---|
| Single atomic value | Multiple internal counters |
| Performs well under low contention | Performs better under high contention |
| CAS retries increase under heavy concurrency | Reduces contention by spreading updates across cells |
| Best for moderate concurrency | Best for high-frequency counters |
Example
Metrics such as API request counts or page views often use LongAdder because many threads update the counter simultaneously.
Interview Tip: Choose LongAdder for high-contention counters and AtomicLong when exact atomic reads after every update are more important.
G. Executor Framework
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Creating a new thread for every task is expensive because thread creation, scheduling, and destruction consume time and memory.
ExecutorService manages a pool of reusable threads, allowing tasks to be executed efficiently without creating new threads each time.
Benefits:
Thread reuse
Better performance
Controlled concurrency
Task scheduling
Graceful shutdown
Supports Callable, Future, and CompletableFuture
Example
Instead of:
new Thread(task).start();
Use:
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(task);
Interview Tip: In production, we rarely create threads manually. We use ExecutorService because it manages thread lifecycle and improves performance.
A Thread Pool is a collection of pre-created worker threads that execute submitted tasks.
Instead of creating a new thread for every request, existing threads are reused.
| Advantages | Faster execution |
|---|---|
| Lower memory usage | Reduced thread creation overhead |
| Better CPU utilization | Limits the maximum number of concurrent threads |
| Real-world Example | A web server handling thousands of HTTP requests uses a thread pool instead of creating thousands of threads. |
Interview Tip: Thread pools improve scalability by reusing threads instead of constantly creating and destroying them.
Java provides several commonly used thread pools.
| Thread Pool | Purpose |
|---|---|
| FixedThreadPool | Fixed number of threads |
| CachedThreadPool | Creates threads as needed |
| SingleThreadExecutor | One worker thread |
| ScheduledThreadPool | Executes delayed or periodic tasks |
| WorkStealingPool | Parallel task execution (Java 8+) |
Interview Tip: Choose the thread pool based on workload characteristics rather than using one pool for everything.
A Fixed Thread Pool has a fixed number of worker threads.
Extra tasks wait in a queue.
Threads are reused.
Example
ExecutorService executor = Executors.newFixedThreadPool(5);
Use Cases
Web servers
Database operations
REST API requests
Interview Tip: FixedThreadPool is the most commonly used pool in enterprise applications because it provides predictable resource usage.
A Cached Thread Pool:
- Creates new threads when needed.
- Reuses idle threads.
- Removes idle threads after a timeout.
Example
ExecutorService executor = Executors.newCachedThreadPool();
Best For
Short-lived asynchronous tasks.
Not suitable when task volume is unbounded because it can create a very large number of threads.
Interview Tip: CachedThreadPool is flexible but can exhaust system resources if too many tasks arrive simultaneously.
It uses exactly one worker thread.
Tasks execute:
- Sequentially
- In submission order
Example
ExecutorService executor = Executors.newSingleThreadExecutor();
Use Cases
Logging
File writing
Sequential processing
Interview Tip: SingleThreadExecutor guarantees task ordering.
It executes tasks:
- After a delay
- At fixed intervals
Example
ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
Use Cases
Cache cleanup
Health checks
Periodic reports
Session cleanup
Interview Tip: ScheduledExecutorService is the modern replacement for Timer.
| shutdown() | shutdownNow() |
|---|---|
| Stops accepting new tasks | Stops accepting new tasks |
| Allows running tasks to finish | Attempts to interrupt running tasks |
| Graceful shutdown | Immediate shutdown (best effort) |
| Preferred | Use only when immediate termination is required |
Interview Tip: shutdown() is graceful, while shutdownNow() is a best-effort immediate shutdown and may interrupt running threads.
| execute() | submit() |
|---|---|
| Returns void | Returns Future |
| Only Runnable | Runnable or Callable |
| Exception handled by thread’s uncaught exception handler | Exception captured in Future |
| Cannot retrieve result | Can retrieve result |
Interview Tip: Use submit() when you need the task result or exception; otherwise execute() is sufficient.
H. Future & CompletableFuture
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Callable is similar to Runnable, but:
- Returns a value
- Can throw checked exceptions
- Used with ExecutorService
Example
Callable<Integer> task = () -> 100;
Interview Tip: Runnable performs a task, while Callable performs a task and returns a result.
Future represents the result of an asynchronous computation.
It allows you to:
- Check if the task is complete
- Wait for completion
- Retrieve the result
- Cancel the task
Example
Future<Integer> future = executor.submit(task);
Integer result = future.get();
Interview Tip: Future lets you retrieve the result of a background task.
Future has several limitations:
- get() blocks the calling thread.
- Difficult to chain multiple asynchronous tasks.
- No built-in callback support.
- Combining multiple futures is cumbersome.
- These limitations led to the introduction of CompletableFuture.
Interview Tip: CompletableFuture solves the limitations of Future by enabling non-blocking composition.
CompletableFuture is an enhanced asynchronous programming API introduced in Java 8.
Features:
Non-blocking
Task chaining
Combining multiple tasks
Exception handling
Parallel execution
Interview Tip: CompletableFuture is the preferred API for asynchronous programming in modern Java.
thenApply() transforms the result of a completed task into another value.
Example
future.thenApply(String::toUpperCase);
Think of it like map() in Java Streams.
Interview Tip: thenApply() transforms the result without starting another asynchronous task.
thenCompose() chains asynchronous tasks where the second task depends on the result of the first.
Think of it as flatMap() for CompletableFuture.
Interview Tip: Use thenCompose() when the next step itself returns another CompletableFuture.
thenCombine() combines the results of two independent CompletableFutures.
Example
Fetch customer details.
Fetch order details.
Combine both results.
Interview Tip: thenCombine() is used when two asynchronous tasks can run independently.
exceptionally() provides a fallback value when an asynchronous computation fails.
Example
future.exceptionally(ex -> "Default");
Interview Tip: exceptionally() allows graceful recovery from asynchronous failures.
allOf()
Waits for all futures to complete.
anyOf()
Returns as soon as any one future completes.
Interview Tip: allOf() waits for everyone; anyOf() returns the fastest result.
I. Concurrent Collections
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
ConcurrentHashMap is a thread-safe implementation of Map that allows multiple threads to read and update concurrently with much better scalability than Hashtable.
Features:
- Thread-safe
- High concurrency
- Does not lock the entire map for every operation
- Better performance than Hashtable
Interview Tip: ConcurrentHashMap achieves thread safety with finer-grained synchronization, allowing multiple threads to operate concurrently.
| Feature | HashMap |
|---|---|
| Hashtable | ConcurrentHashMap |
| Thread-safe | No |
| Yes | Yes |
| Locking | None |
| Entire table | Fine-grained locking / concurrent design |
| Performance | Fastest (single-threaded) |
| Slow | High concurrency |
| Null key/value | One null key, multiple null values |
| No null keys or values | No null keys or values |
Interview Tip: For concurrent applications, prefer ConcurrentHashMap over Hashtable because it offers much better scalability.
CopyOnWriteArrayList creates a new copy of the underlying array whenever it is modified.
Advantages:
- Thread-safe iteration
- Readers are never blocked by writers
Disadvantage:
- Writes are expensive because each modification copies the entire array
Best for:
- Read-heavy, write-light workloads
Interview Tip: Use CopyOnWriteArrayList when reads greatly outnumber writes.
BlockingQueue is a thread-safe queue where:
- Producers wait if the queue is full.
- Consumers wait if the queue is empty.
- It is widely used in producer-consumer implementations and thread pools.
Interview Tip: BlockingQueue simplifies producer-consumer implementations by handling waiting internally.
ConcurrentLinkedQueue is a lock-free, thread-safe queue based on CAS.
Features:
- Non-blocking
- High throughput
- FIFO ordering
- Best for high-concurrency scenarios where blocking is unnecessary.
Interview Tip: ConcurrentLinkedQueue is ideal for high-performance, non-blocking queues.
ConcurrentSkipListMap is a thread-safe, sorted map.
Features:
- Keys remain sorted.
- Concurrent reads and writes.
- Better scalability than synchronizing a TreeMap.
Interview Tip: Choose ConcurrentSkipListMap when you need both sorted keys and thread safety.
J. Deadlock & Concurrency Problems
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A deadlock occurs when two or more threads are permanently blocked because each thread is waiting for a lock held by another thread.
Example
Thread A holds Lock 1 and waits for Lock 2.
Thread B holds Lock 2 and waits for Lock 1.
Neither thread can proceed.
Interview Tip: Deadlock is a permanent waiting state caused by circular lock dependencies.
Common strategies include:
- Acquire locks in a consistent order.
- Avoid nested locking where possible.
- Use tryLock() with a timeout.
- Keep synchronized blocks small.
- Minimize the number of locks.
Interview Tip: The most common prevention technique is to always acquire multiple locks in the same order.
A livelock occurs when threads are not blocked, but they continuously respond to each other and keep changing state without making any useful progress.
Unlike deadlock, the threads remain active but never complete their work.
Interview Tip: Deadlock means threads are waiting; livelock means threads are busy but making no progress.
Starvation occurs when a thread waits indefinitely because other threads continuously receive access to the required resource.
Possible causes:
- Unfair scheduling
- High-priority threads dominating execution
- Poor locking strategy
Interview Tip: Starvation happens when a thread never gets a fair chance to execute.
A race condition occurs when multiple threads update shared mutable data concurrently without proper synchronization, making the final result depend on the execution order.
Example
count++; // Read -> Increment -> Write
If two threads execute this statement simultaneously, one increment can be lost because count++ is not an atomic operation.
To prevent race conditions, use:
- synchronized
ReentrantLock
AtomicInteger
Other concurrency utilities
Interview Tip: Race conditions arise from unsynchronized access to shared mutable state, leading to unpredictable results.
K. Java 8 Parallel Processing
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A Parallel Stream is a Java 8 Stream that processes elements concurrently using multiple threads. It internally uses the Fork/Join Framework and the common ForkJoinPool to split the work across available CPU cores.
Example
list.parallelStream()
.forEach(System.out::println);
When to use?
| CPU-intensive operations | Large datasets |
|---|---|
| Independent operations (no shared mutable state) | When NOT to use? |
| Small collections | I/O-bound tasks |
| Operations with side effects | When maintaining encounter order is critical |
Interview Tip: Parallel Stream improves performance only when the workload is large enough and tasks are independent.
| Sequential Stream | Parallel Stream |
|---|---|
| Uses a single thread | Uses multiple threads |
| Executes elements one after another | Executes elements concurrently |
| Easier to debug | More complex to debug |
| Predictable ordering | Ordering may not be preserved unless explicitly requested |
| Lower overhead | Has thread management overhead |
Example
list.stream();
list.parallelStream();
Interview Tip: Don’t assume parallel streams are always faster. Measure performance before using them.
The Fork/Join Framework, introduced in Java 7, is designed for efficiently executing tasks that can be recursively divided into smaller subtasks.
It follows this approach:
- Fork → Split a large task into smaller tasks.
- Execute subtasks in parallel.
- Join → Combine the results.
It is the foundation for:
- Parallel Streams
- RecursiveTask
- RecursiveAction
- Real-world Example
- Processing a large file by dividing it into chunks and processing each chunk concurrently.
Interview Tip: Fork/Join follows the Divide-and-Conquer algorithm.
The Work-Stealing Algorithm improves CPU utilization in the Fork/Join Framework.
Each worker thread maintains its own queue of tasks.
If a worker finishes its tasks, it steals tasks from another worker’s queue instead of remaining idle.
Benefits:
- Better load balancing
- Improved CPU utilization
- Reduced idle time
Interview Tip: Work stealing keeps all CPU cores busy by allowing idle threads to steal work from busy threads.
L. Java 21 Virtual Threads
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Virtual Threads are lightweight threads introduced as a standard feature in Java 21 (Project Loom).
Unlike traditional platform threads, virtual threads are managed by the JVM instead of the operating system, allowing applications to create millions of concurrent threads with very low memory overhead.
Example
Thread.startVirtualThread(() -> {
System.out.println("Virtual Thread");
});
Interview Tip: Virtual threads make thread-per-request architectures practical again.
| Platform Thread | Virtual Thread |
|---|---|
| Managed by OS | Managed by JVM |
| Heavyweight | Lightweight |
| Limited by OS resources | Millions can be created |
| Higher memory usage | Very low memory usage |
| Suitable for CPU-bound tasks | Excellent for blocking I/O tasks |
Interview Tip: Platform threads are expensive OS threads; virtual threads are lightweight JVM-managed threads.
Virtual Threads are ideal for applications with many concurrent blocking operations, such as:
| REST APIs | Database calls |
|---|---|
| File I/O | HTTP clients |
| Microservices | Avoid using them as a performance optimization for CPU-bound computations. |
Interview Tip: Virtual threads shine for I/O-bound workloads, not CPU-intensive computations.
Advantages:
- Extremely lightweight
- Supports millions of concurrent tasks
- Simplifies asynchronous programming
- Better scalability for blocking I/O
- Easier to read and maintain than callback-based code
Interview Tip: Virtual threads improve scalability while allowing developers to write simple synchronous code.
Limitations:
- Do not make CPU-bound tasks faster.
- Excessive synchronized blocks or native calls can pin a virtual thread to its carrier platform thread, reducing scalability.
- Existing thread-pool tuning strategies may need to be reconsidered.
- Applications should still avoid unnecessary thread creation and contention.
Interview Tip: Virtual threads reduce the cost of blocking, but they don’t eliminate the need for good concurrency design.
M. Coding Questions
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Two threads coordinate using wait() and notify() (or Lock/Condition) so that:
- Thread 1 prints odd numbers.
- Thread 2 prints even numbers.
- Output remains ordered.
Interview Tip: Tests synchronization and inter-thread communication.
Implement a producer and consumer sharing a bounded buffer using:
- wait() / notifyAll()
- BlockingQueue (preferred in production)
Interview Tip: Mention that BlockingQueue is usually preferred because it handles synchronization internally.
Create two locks:
- Thread A locks Object 1 then waits for Object 2.
- Thread B locks Object 2 then waits for Object 1.
- This demonstrates circular waiting.
- Follow-up
- Explain how to avoid it using consistent lock ordering or tryLock().
The preferred approach is the Initialization-on-Demand Holder idiom or an Enum Singleton.
Double-checked locking with a volatile instance is also acceptable.
Interview Tip: Mention that volatile is required in double-checked locking to prevent instruction reordering.
One thread prints:
- 1, 3, 5…
The other prints:
- 2, 4, 6…
- Synchronization ensures correct ordering.
Interview Tip: Similar concept to the Odd-Even problem.
Three threads coordinate to print:
- ABCABCABCABC…
Can be implemented using:
- wait() / notifyAll()
ReentrantLock with multiple Conditions
Semaphores
Interview Tip: This question evaluates thread coordination among more than two threads.
Typical implementation:
- Create a thread pool.
- Submit tasks.
- Shut down the executor gracefully.
- Interviewers mainly check whether you use ExecutorService correctly instead of creating threads manually.
Typical flow:
- Create a Callable.
- Submit it to an ExecutorService.
- Retrieve the result using Future.get().
- Follow-up
- Be ready to explain that get() blocks until the computation completes.
Typical implementation:
- Use CompletableFuture.supplyAsync().
- Chain operations with thenApply().
- Handle failures with exceptionally().
Interview Tip: Interviewers often ask how to chain asynchronous tasks without blocking.
CountDownLatch allows one or more threads to wait until a fixed number of operations complete.
Common use cases:
- Waiting for multiple services during startup.
- Waiting for multiple worker threads before continuing.
Interview Tip: CountDownLatch is one-time use. Once the count reaches zero, it cannot be reset.
Implement a producer-consumer system where:
- Producer inserts using put().
- Consumer retrieves using take().
- The queue automatically blocks producers when full and consumers when empty.
Interview Tip: Prefer BlockingQueue over manually using wait() and notify() in production code because it is simpler and less error-prone.
N. Frequently Asked Scenario Questions
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A class is thread-safe if it behaves correctly when accessed by multiple threads simultaneously.
Common approaches:
- Synchronize access to shared mutable state using synchronized or ReentrantLock.
- Use immutable objects wherever possible.
- Use concurrent collections like ConcurrentHashMap.
- Use atomic classes such as AtomicInteger.
- Minimize shared mutable state.
Example
public class Counter {
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
}
Interview Tip: My first preference is to reduce shared mutable state. If sharing is unavoidable, I use the appropriate synchronization mechanism.
String is immutable because its value cannot be changed after creation.
Since its state never changes:
- Multiple threads can safely share the same String.
- No synchronization is required.
- There is no possibility of race conditions.
Immutability also improves:
- Security
- String Pool efficiency
- Hash code caching
- Performance
Interview Tip: Immutability naturally provides thread safety because there are no state changes to synchronize.
HashMap does not synchronize read or write operations.
If multiple threads modify it simultaneously, problems such as:
- Race conditions
- Data corruption
- Lost updates
- In older JDKs, issues during resizing
- can occur.
- Therefore, HashMap should not be shared between multiple writing threads without external synchronization.
Interview Tip: HashMap is designed for single-threaded use. For concurrent access, prefer ConcurrentHashMap.
ConcurrentHashMap provides thread safety while allowing high concurrency.
Internally it:
- Allows concurrent reads without locking in most cases.
- Uses fine-grained synchronization and CAS (Compare-And-Swap) for updates instead of locking the entire map.
- Locks only the affected bucket or bin when necessary, reducing contention.
- As a result, multiple threads can operate on different parts of the map simultaneously.
Interview Tip: ConcurrentHashMap improves scalability by avoiding a single global lock.
For a production cache, I would typically use:
- ConcurrentHashMap for thread-safe storage.
- An eviction strategy such as LRU or LFU.
- TTL (Time-To-Live) for expiration.
- ScheduledExecutorService or a caching library for cleanup.
- Atomic operations like computeIfAbsent() to avoid duplicate cache population.
For enterprise applications, I would generally use mature solutions like Caffeine for local caching or Redis for distributed caching instead of building a cache from scratch.
Interview Tip: In production, I prefer proven caching libraries rather than implementing my own cache.
Spring Boot uses multithreading extensively.
Examples include:
- Embedded servers (Tomcat, Jetty, Undertow) use thread pools to handle HTTP requests.
- @Async executes methods asynchronously.
ExecutorService or ThreadPoolTaskExecutor manages background tasks.
CompletableFuture enables asynchronous workflows.
Kafka and RabbitMQ consumers process messages concurrently.
Scheduled jobs use @Scheduled.
Interview Tip: Every incoming HTTP request is typically handled by a thread from the server’s request-processing thread pool.
I would avoid creating one thread per record.
A scalable approach would be:
- Split the data into batches (for example, 1,000–10,000 records per batch).
- Use a bounded ExecutorService or ThreadPoolTaskExecutor.
- Submit each batch as a task.
- Process batches concurrently.
- Wait for completion using CompletableFuture.allOf(), CountDownLatch, or similar mechanisms.
- Tune the thread pool size based on CPU cores for CPU-bound work or expected I/O concurrency for I/O-bound work.
- This approach provides controlled concurrency without overwhelming the JVM or the database.
Interview Tip: Parallelize by batching work and using a bounded thread pool—not by creating a thread for every record.
I would never create unlimited threads.
Common approaches include:
- Using a fixed-size ExecutorService.
Configuring ThreadPoolExecutor with:
- Core pool size
- Maximum pool size
- Queue capacity
- Applying back-pressure when the queue becomes full.
- Using bounded queues to prevent excessive task accumulation.
Interview Tip: A thread pool with sensible limits protects the application from resource exhaustion.
Creating too many threads can lead to:
- High memory consumption (each thread requires stack memory).
- Excessive context switching.
- Increased CPU scheduling overhead.
- Reduced throughput.
- OutOfMemoryError if thread creation exceeds system limits.
- Overall performance degradation.
- Instead, reuse threads through thread pools.
Interview Tip: More threads do not always mean better performance. Beyond a certain point, they increase overhead instead of improving throughput.
A typical production approach would be:
Capture a thread dump using tools such as:
- jstack
- jcmd
- Java Flight Recorder (JFR)
- Look for threads in the BLOCKED state.
- Identify circular lock dependencies (Thread A waiting for Thread B’s lock and vice versa).
- Analyze stack traces to find the synchronized blocks or lock acquisitions involved.
Fix the code by:
- Acquiring locks in a consistent order.
- Reducing nested locking.
- Using tryLock() with timeouts where appropriate.
Interview Tip: My first step is to capture and analyze a thread dump. It clearly shows blocked threads and lock ownership.
Additional Scenario Questions Often Asked (Senior Level)
The optimal size depends on the workload.
CPU-bound tasks: Use approximately the number of available CPU cores (or cores + 1 in some cases).
I/O-bound tasks: Use a larger pool because many threads spend time waiting on external resources.
The pool size should be based on benchmarking and monitoring rather than arbitrary values.
Interview Tip: Thread pool sizing depends on whether the workload is CPU-bound or I/O-bound.
Use Parallel Streams when processing large in-memory collections with independent, CPU-intensive operations.
Use CompletableFuture when orchestrating asynchronous workflows, such as calling multiple REST services or databases in parallel, handling dependencies, combining results, or managing exceptions.
Interview Tip: Parallel Streams parallelize collection processing, while CompletableFuture coordinates asynchronous tasks.
BlockingQueue encapsulates synchronization internally.
Benefits include:
- Simpler code
- Less error-prone
- No manual wait()/notify() handling
- Better readability
- Production-ready producer-consumer implementation
Interview Tip: In production code, I prefer higher-level concurrency utilities over manual thread coordination.