Concurrency & Multithreading
- What is the difference between synchronized and ReentrantLock? When would you prefer ReentrantLock?
- Explain the Java Memory Model (JMM). What is volatile and how does it differ from synchronized?
- How does ConcurrentHashMap work internally? Why is it better than Collections.synchronizedMap()?
- Explain different ways to create threads in Java. Which one do you prefer in production and why?
- What are CountDownLatch, CyclicBarrier, and Semaphore? Give real-world use cases.
- How do you prevent deadlock? How do you detect it in a running system?
- Explain ThreadLocal. When is it useful and what are the pitfalls?
- What is CompletableFuture? How is it different from Future?
- Explain StampedLock and when you would use it over ReentrantReadWriteLock.
- How does synchronized work internally? (Monitor, Wait Set, Entry Set)
- Explain the Executor Framework in detail. Why is it preferred over creating threads manually?
- Explain ThreadPoolExecutor parameters and how to configure it properly.
- Explain different ExecutorService factory methods in Executors class and their p
- ros/cons.
- How do you handle graceful shutdown of ExecutorService?
- What are common pitfalls with Thread Pools in high-load systems?
- What is Multithreading and Concurrency? Why is it important in backend systems?
- How does Java support Multithreading? Different ways to create threads?
- Explain ExecutorService and ThreadPoolExecutor in detail.
- What is the difference between synchronized and ReentrantLock? When do you use each?
- Explain volatile keyword. How does it relate to Java Memory Model (JMM)?
- How does ConcurrentHashMap work internally? Why prefer it?
- What are CountDownLatch, CyclicBarrier, Semaphore, and Phaser?
- What is ThreadLocal? Pros, Cons, and Use Cases
- What is Deadlock? How to prevent and detect it?
- Explain CompletableFuture. How do you use it for asynchronous processing?
- How do you handle concurrency in Spring Boot applications
- What are common concurrency issues you faced and how did you resolve them? (Behavioral + Technical)
- Advanced: What are Virtual Threads (Project Loom) in Java 21?
- Difference between yield(), sleep(), join(), wait(), and notify()?
- Explain CompletableFuture and its advantages over Future.
- How do you handle concurrency in Spring Boot applications?
- What concurrency issues have you faced in real projects?
- Advanced: Virtual Threads (Java 21+)
- Bonus: Difference between submit() and execute() in ExecutorService?
- What is ExecutorService?
- What is CompletableFuture?
- CompletableFuture vs ExecutorService ?
- What is the difference between thenApply() and thenCompose()?
- In a microservices environment, how do you handle distributed concurrency? (e.g., two services updating same wallet balance)"
- How do you debug a high CPU usage issue caused by too many threads or deadlocks in production?
- When to use ExecutorService? When to use CompletableFuture class ?
What is the difference between synchronized and ReentrantLock? When would you prefer ReentrantLock?
=> synchronized is a keyword that provides implicit locking. It acquires the intrinsic lock of the object.
=> ReentrantLock is explicit, more flexible, and part of java.util.concurrent.
=> ReentrantLock supports fairness (new ReentrantLock(true)), tryLock() with timeout, lockInterruptibly(), and multiple condition variables via Condition objects.
=> synchronized is easier but less flexible. You cannot interrupt a thread waiting for a synchronized block easily.
=> ReentrantLock requires manual unlock() in finally block (critical for avoiding deadlocks).
When to prefer ReentrantLock (Senior-level expectation):
=> Need fairness policy.
=> Need timed or interruptible lock acquisition.
=> Need multiple wait conditions on the same lock (like producer-consumer with different conditions).
=> Fine-grained control in complex concurrent systems (e.g., in Payment systems where you have multiple resource locks).
=> Real Interview Tip: Interviewers often ask — “Have you faced a situation where synchronized was not enough?” Be ready with a real example.
Explain the Java Memory Model (JMM). What is volatile and how does it differ from synchronized?
=> JMM defines how threads interact through memory and guarantees happens-before relationships.
volatile key points:
=> Ensures visibility: Changes to a volatile variable are immediately visible to other threads.
=> Prevents instruction reordering for that variable.
=> Does not provide atomicity for compound operations (e.g., i++ is still not atomic).
Comparison with synchronized:
=> volatile → Only visibility + ordering.
=> synchronized → Visibility + Atomicity + Mutual exclusion.
Use cases for volatile:
=> Flag variables (isShutdown, isInitialized).
=> Double-checked locking (with care in Java 5+).
Senior Expectation: Explain happens-before rules (e.g., unlock → lock, write to volatile → subsequent read, etc.).
How does ConcurrentHashMap work internally? Why is it better than Collections.synchronizedMap()?
=> In Java 8+, ConcurrentHashMap uses segmentation at a finer level using an array of buckets with CAS (Compare-And-Swap) + synchronized only on specific nodes.
Key improvements:
=> Better concurrency: Multiple threads can write to different buckets simultaneously.
=> Uses lazy initialization of bins and treeification (converts linked list to red-black tree when bin size > 8).
=> size() is approximate (uses sumCount()).
Vs Collections.synchronizedMap():
=> Synchronized map locks the entire map for every operation → poor scalability.
=> ConcurrentHashMap allows high concurrency.
Explain different ways to create threads in Java. Which one do you prefer in production and why?
=> Never say new Thread() or extending Thread class directly.
Best practice:
ExecutorService executor = Executors.newFixedThreadPool(10);
// or better
ThreadPoolExecutor customPool = new ThreadPoolExecutor(...);
Or use CompletableFuture / ForkJoinPool for advanced cases.
Reasons:
=> Better resource management (thread reuse).
=> Configurable queue (e.g., LinkedBlockingQueue vs SynchronousQueue).
=> Proper exception handling via ThreadFactory + UncaughtExceptionHandler.
What are CountDownLatch, CyclicBarrier, and Semaphore? Give real-world use cases.
| Class | Reusable? | Use Case (Fintech/Payment Context) |
|---|---|---|
| CountDownLatch | No | Wait for multiple payment verification services |
| CyclicBarrier | Yes | Multiple threads processing parts of a batch settlement |
| Semaphore | Yes | Rate limiting concurrent API calls to external bank |
How do you prevent deadlock? How do you detect it in a running system?
Prevention techniques:
=> Resource ordering (always acquire locks in same order).
=> Try-lock with timeout.
=> Lock timeout + rollback.
=> Use higher-level constructs (ConcurrentHashMap, StampedLock).
Detection in production:
=> Thread dump analysis (jstack, VisualVM, or kill -3).
=> Look for circular waiting in stack traces.
=> Tools: jcmd, Flight Recorder, or monitoring tools like Dynatrace/New Relic.
Real Example:
In payment systems, deadlock can occur between userLock and walletLock if not ordered properly.
Explain ThreadLocal. When is it useful and what are the pitfalls?
=> ThreadLocal provides per-thread copy of a variable.
Common Use Cases:
=> Storing UserContext, TransactionContext, MDC (Mapped Diagnostic Context) for logging.
=> Database connection per thread (though not recommended now with HikariCP).
Pitfalls (Senior level):
=> Memory leaks in thread pools (must call remove() in finally).
=> Not suitable for request-scoped data in modern reactive/Spring WebFlux.
What is CompletableFuture? How is it different from Future?
=> CompletableFuture (Java 8+) supports chaining, combining, and exception handling functionally.
Key methods:
thenApply(), thenCompose(), thenCombine()
exceptionally(), handle()
allOf(), anyOf()
Real Use Case (Fintech):
Combining results from multiple payment gateway calls asynchronously.
CompletableFuture.allOf(future1, future2)
.thenRun(() -> processResult());
Explain StampedLock and when you would use it over ReentrantReadWriteLock.
=> StampedLock supports optimistic reading — threads can read without acquiring lock and validate stamp later.
=> Better performance than ReentrantReadWriteLock when read-heavy with occasional writes.
How does synchronized work internally? (Monitor, Entry Set, Wait Set)
Expect deep JVM knowledge:
=> Every object has a monitor.
=> Threads go to Entry Set → acquire lock → Owner.
=> If wait() called → moves to Wait Set.
--------------------------------------------------------------------------------------------------------
Revision Strategy
Must Master (Daily): Questions 1, 2, 3, 6, 7
Strong Understanding: 4, 5, 8
Advanced (Differentiation): 9, 10
--------------------------------------------------------------------------------------------------------
Explain the Executor Framework in detail. Why is it preferred over creating threads manually?
=> The Executor Framework (introduced in Java 5) decouples task submission from task execution.
=> It manages thread lifecycle, reuse, queuing, and graceful shutdown.
Core Interfaces:
=> Executor → Simple execute(Runnable)
=> ExecutorService → Adds submit(), shutdown(), shutdownNow(), awaitTermination(), invokeAll(), invokeAny()
=> ScheduledExecutorService → For delayed and periodic tasks
Why prefer over new Thread() ?
=> Thread creation is expensive (OS-level call).
=> Uncontrolled thread creation can crash the application (OutOfMemoryError: unable to create new native thread).
=> No built-in mechanism for queuing, monitoring, or controlled shutdown.
=> Poor exception handling.
=> In production (especially Payment/Wallet systems), you must use thread pools.
Explain ThreadPoolExecutor parameters and how to configure it properly.
=> This is one of the most asked follow-up questions at senior level.
Java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 1
maximumPoolSize, // 2
keepAliveTime, // 3
unit, // 4
workQueue, // 5
threadFactory, // 6
handler // 7 (RejectedExecutionHandler)
);
Detailed Explanation of Each Parameter:
- corePoolSize: Minimum number of threads that are always alive (even if idle).
- maximumPoolSize: Maximum threads allowed. Extra threads are created only when queue is full.
- keepAliveTime: Time that excess idle threads (above corePoolSize) will wait before termination.
- workQueue:
=> LinkedBlockingQueue (unbounded) → Common but can lead to OOM if tasks grow.
=> ArrayBlockingQueue (bounded) → Better for backpressure.
=> SynchronousQueue → Used in newCachedThreadPool() (no queuing, direct handoff). - ThreadFactory: Custom thread naming (very useful for debugging thread dumps).
- RejectedExecutionHandler:
=> CallerRunsPolicy (default in many cases) → Executes in caller thread (good for backpressure).
=> AbortPolicy → Throws RejectedExecutionException.
=> DiscardPolicy / DiscardOldestPolicy.
Real Interview Answer Tip:
In my payment wallet project, I used corePoolSize = 10, maxPoolSize = 50, LinkedBlockingQueue with capacity 100, and CallerRunsPolicy. This ensured that during payment spikes, the system slows down gracefully instead of rejecting requests.
What is Multithreading and Concurrency? Why is it important in backend systems?
=> Multithreading means multiple threads executing within a single process. Concurrency is about managing multiple tasks that may or may not run simultaneously (can be on single or multi-core).
Why critical at senior level?
=> High throughput in Payment systems (handle 1000s of transactions/sec).
=> Better CPU utilization.
=> Responsiveness (non-blocking I/O).
Interview Tip: Mention trade-offs — increased complexity, race conditions, deadlocks, debugging difficulty.
How does Java support Multithreading? Different ways to create threads?
Two traditional ways:
=> Extend Thread class
=> Implement Runnable interface
=> ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> { /* task */ });
=> Best Practice: Always use Executor Framework (never raw new Thread() in production).
Explain ExecutorService and ThreadPoolExecutor in detail.
=> ExecutorService is the interface for submitting tasks. ThreadPoolExecutor is the concrete implementation.
Key Parameters (Must explain with reasoning):
new ThreadPoolExecutor(
corePoolSize, // Threads always alive (e.g., 8-16 for payment service)
maximumPoolSize, // Max threads during peak (e.g., 64)
keepAliveTime, // Idle time for extra threads
TimeUnit.SECONDS,
workQueue, // LinkedBlockingQueue / ArrayBlockingQueue
threadFactory, // Custom naming for debugging
handler // RejectedExecutionHandler
);
Configuration Strategy for Payment Wallet System:
=> corePoolSize = Number of CPU cores or slightly higher.
=> Use bounded queue (ArrayBlockingQueue) to apply backpressure.
=> CallerRunsPolicy as handler — slows down incoming requests during overload instead of failing.
Graceful Shutdown:
executor.shutdown();
executor.awaitTermination(30, TimeUnit.SECONDS);
if (!executor.isTerminated()) executor.shutdownNow();
Memory leaks in thread pools → Always call threadLocal.remove() in finally.
=> ThreadLocal gives each thread its own copy of variable.
Common uses:
=> Storing SecurityContext, TransactionId, MDC for logging.
=> Per-thread SimpleDateFormat (legacy)
Major Pitfall:
=> Memory leaks in thread pools → Always call threadLocal.remove() in finally.
How do you handle concurrency in Spring Boot applications?
=> Use @Async with custom ThreadPoolTaskExecutor.
=>Configure in @Configuration
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Async-Payment-");
executor.initialize();
return executor;
}
=> For transactions: @Transactional + proper isolation level (REPEATABLE_READ for payments)
=> Use CompletableFuture inside services.
=> For reactive: WebFlux + Reactor.
What are common concurrency issues you faced and how did you resolve them? (Behavioral + Technical)
In my Payment Wallet System, we had race conditions while updating wallet balance during high load
Solution: Used ReentrantLock with proper ordering + optimistic locking at database level (@Version in JPA).
Also implemented distributed locks via Redis for cross-service consistency.
Advanced: What are Virtual Threads (Project Loom) in Java 21
=> Virtual threads are lightweight threads managed by JVM (not OS). Thousands/millions can be created cheaply.
Benefits
=> No callback hell or complex async code.
=> Simple blocking code scales well.
=> When to use: I/O bound workloads (most backend services). Not for CPU-bound.
=> Current Status (2026): Widely adopted in Spring Boot 3.2+ for better scalability.
Difference between yield(), sleep(), join(), wait(), and notify()
| Method | Purpose | Belongs To | Releases Lock? | Can be Interrupted? | Use Case (Payment System) |
|---|---|---|---|---|---|
yield() | Hint to scheduler to give CPU to other threads | Thread | No | No | Low-priority background tasks (e.g., logging) |
sleep(long) | Pause current thread for time | Thread | No | Yes | Retry delays, polling |
join() | Wait for another thread to complete | Thread | No | Yes | Wait for parallel payment validations |
wait() | Release lock and wait for notification | Object | Yes | Yes | Producer-Consumer patterns |
notify() / notifyAll() | Wake up waiting threads | Object | No (until lock released) | - | Signal completion of critical section |
=> yield() is not guaranteed — scheduler may ignore it. Rarely used in real production code.
=> sleep() keeps the lock (if inside synchronized), can cause performance issues.
=> join() is used when one thread depends on another’s result.
=> wait()/notify() must be used inside a synchronized block and usually in a while loop (to handle spurious wakeups).
=> Senior Tip: "I prefer higher-level constructs (CountDownLatch, CompletableFuture) over raw wait()/notify() in modern code."
CountDownLatch Real usecase ?
Real Example: Use CountDownLatch to wait for multiple external services (bank, KYC, fraud) before processing a transaction.
Bonus: Difference between submit() and execute() in ExecutorService?
=> execute(Runnable): Fire and forget, no result.
=> submit(Callable): Returns Future for result + exception handling.
How do you debug a high CPU usage issue caused by too many threads or deadlocks in production?
=> Take multiple thread dumps (jstack -l <pid> or kill -3 <pid>) 3–4 times with 5–10 sec gap.
=> Analyze with tools like jvisualvm, FastThread, or Eclipse MAT.
=> Look for threads in RUNNABLE state with high CPU or blocked on same locks.
=> Check for ThreadLocal leaks or improper pool sizing.
In a microservices environment, how do you handle distributed concurrency? (e.g., two services updating same wallet balance)"
=> Optimistic locking (JPA @Version).
=> Pessimistic locking (with care).
=> Distributed locks using Redis (Redisson or Lettuce with tryLock()).
=> Saga pattern or eventual consistency for non-critical flows.
=> Idempotency keys for payments.
When to use ExecutorService? When to use CompletableFuture class ?
Use ExecutorService when:
=> You need fine-grained control over thread pool (core/max size, queue behavior, rejection policy).
=> Simple fire-and-forget tasks.
=> Legacy code or Java < 8.
Use CompletableFuture when:
=> You need to orchestrate multiple async operations (parallel + sequential).
=> You want clean, readable async code (functional style).
=> Complex flows with exception handling and chaining (very common in Payment systems: balance → limit → fraud → notification).
Best Practice:
=> Use a custom ThreadPoolExecutor for control.
=> Run your CompletableFuture on that executor.
=> In Spring Boot → Use @Async (which uses TaskExecutor) or directly use CompletableFuture