0% found this document useful (0 votes)
1 views9 pages

Java Multithreading Concurrency Interview Questions

This document is a comprehensive guide for backend developers preparing for interviews, focusing on Java multithreading and concurrency topics. It covers essential concepts such as thread lifecycle, synchronization, concurrent collections, and advanced concurrency utilities, along with practical interview questions and answers. The guide is structured into sections addressing various aspects of multithreading, including real-world scenarios and tricky output-based questions.

Uploaded by

Ritu Singh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views9 pages

Java Multithreading Concurrency Interview Questions

This document is a comprehensive guide for backend developers preparing for interviews, focusing on Java multithreading and concurrency topics. It covers essential concepts such as thread lifecycle, synchronization, concurrent collections, and advanced concurrency utilities, along with practical interview questions and answers. The guide is structured into sections addressing various aspects of multithreading, including real-world scenarios and tricky output-based questions.

Uploaded by

Ritu Singh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Java Multithreading & Concurrency

Interview Questions — Theory, Scenario-Based &


Advanced
A complete topic-wise guide for backend developers (2-4 years experience) preparing for startup
and product-based company interviews. Covers fundamentals, synchronization, concurrent
collections, the Executor framework, modern concurrency utilities, and tricky output-based
questions — each with a clear answer/approach.

1. Thread Basics & Lifecycle


2. Synchronization, Locks & Race Conditions
3. wait()/notify(), Deadlock, Livelock & Starvation
4. Concurrent Collections
5. Executor Framework & Thread Pools
6. Advanced Concurrency Utilities (CompletableFuture, ThreadLocal, Latches, Semaphores)
7. Java Memory Model & volatile
8. Real-World Scenario Questions
9. Tricky Output-Based Questions
1. Thread Basics & Lifecycle

Q1. What are the different ways to create a thread in Java?


Tests: Fundamentals
Three main ways: (1) Extend the Thread class and override run(). (2) Implement the Runnable interface
and pass it to a Thread (preferred, since Java supports only single inheritance and implementing an
interface keeps the class free to extend something else). (3) Implement Callable<V> when the task needs
to return a result or throw a checked exception, submitted via an ExecutorService and tracked with a
Future.

Q2. What is the thread lifecycle in Java?


Tests: Fundamentals
A thread moves through these states ([Link]): NEW (created, not started) → RUNNABLE
(started, executing or ready to run) → BLOCKED (waiting to acquire a monitor lock) / WAITING or
TIMED_WAITING (waiting due to wait()/join()/sleep()) → TERMINATED (run() completed or threw an
exception). Note there's no separate 'Running' state in the enum — it's part of RUNNABLE.

Q3. What is the difference between Runnable and Callable?


Tests: Fundamentals
[Link]() returns void and cannot throw checked exceptions. Callable<V>.call() returns a value of
type V and can throw checked exceptions. Callable is used with [Link](), which returns a
Future you can use to retrieve the result or catch exceptions via ExecutionException.

Q4. What is a daemon thread, and when would you use one?
Tests: Fundamentals
A daemon thread runs in the background and does not prevent the JVM from exiting once all user
(non-daemon) threads finish — e.g., a background cache-cleanup or heartbeat thread. Set with
[Link](true) before calling start(). If the JVM exits, daemon threads are terminated abruptly
without cleanup, so they shouldn't hold critical resources.

Q5. What is the difference between sleep() and wait()?


Tests: Fundamentals
[Link](ms) pauses the current thread without releasing any locks it holds, and doesn't require a
synchronized context. [Link]() must be called from within a synchronized block/method, releases the
monitor lock while waiting, and is used for inter-thread communication — the thread resumes only when
notified (or times out).

Q6. What is a race condition, and how is it different from a deadlock?


Tests: Scenario-style
A race condition occurs when multiple threads access/modify shared state concurrently and the outcome
depends on unpredictable timing/interleaving of operations (e.g., two threads incrementing a shared counter
and losing an update). A deadlock is when two or more threads are each waiting on a resource the other
holds, so none can proceed — a race condition produces wrong results; a deadlock produces a frozen
application.

2. Synchronization, Locks & Race Conditions


Q7. Two threads are updating a shared counter and the final value is inconsistent after
many increments. Walk through why, and give three ways to fix it.
Tests: Very commonly asked — real production bug
count++ is not atomic — it's actually three steps: read the current value, add 1, write it back. If two threads
interleave between the read and write, one thread's update can be lost. Fixes: (1) wrap the increment in a
synchronized block/method so only one thread executes it at a time. (2) Use
[Link](), which uses CAS (compare-and-swap) at the hardware level — no
locking needed. (3) Use an explicit ReentrantLock around the critical section for more control (tryLock,
fairness, interruptible locking).

AtomicInteger counter = new AtomicInteger(0);


[Link](); // thread-safe, lock-free

Q8. What is the synchronized keyword, and how does it work on methods vs blocks?
Tests: Fundamentals
synchronized ensures only one thread can execute a critical section at a time by acquiring the intrinsic lock
(monitor) of an object. On an instance method, it locks on 'this'. On a static method, it locks on the Class
object. A synchronized block lets you lock on a specific object and limit the locked region to just the
necessary lines, reducing contention compared to synchronizing an entire method.

public void increment() {


synchronized(lockObject) {
count++;
}
}

Q9. What is the difference between synchronized and ReentrantLock?


Tests: Fundamentals
synchronized is simpler (JVM-managed, automatically released even on exception) but inflexible.
ReentrantLock ([Link]) offers: tryLock() with timeout (avoid waiting forever),
interruptible lock acquisition, fairness policy (FIFO ordering of waiting threads), and the ability to check if
the lock is held. Must always be released in a finally block since it's not automatic.

[Link]();
try {
// critical section
} finally {
[Link]();
}

Q10. Why is StringBuffer thread-safe but generally avoided compared to StringBuilder


in backend code?
Tests: Judgment question
StringBuffer synchronizes every method call, which adds locking overhead even when no other thread ever
touches the object — true in almost all string-building scenarios where the StringBuilder/StringBuffer is a
local, method-scoped variable. Use StringBuilder by default; reach for StringBuffer only in the rare case the
same builder instance is genuinely shared and mutated across threads.

3. wait()/notify(), Deadlock, Livelock & Starvation

Q11. What is the difference between wait(), notify(), and notifyAll()?


Tests: Fundamentals
All three must be called from a synchronized context on the object being locked. wait() releases the lock
and suspends the thread until notified. notify() wakes up a single arbitrarily chosen waiting thread.
notifyAll() wakes up all waiting threads, which then compete for the lock — safer in general since notify()
can accidentally wake the wrong thread in a multi-condition wait scenario.

Q12. You suspect a deadlock in a production Java service. How do you detect and
prevent it?
Tests: Real production debugging scenario
Detection: take a thread dump (jstack <pid> or kill -3) — the JVM detects and reports cyclic lock waits
directly in the dump ("Found one Java-level deadlock"). Tools like VisualVM or JConsole can also show this
visually. Prevention: always acquire multiple locks in a consistent, global order across the codebase; prefer
tryLock(timeout) over blocking indefinitely; minimize the scope and nesting of locks; and avoid calling into
unknown code (that might acquire other locks) while already holding a lock.

Q13. What is livelock, and how is it different from deadlock and starvation?
Tests: Often skipped, but a real 'gotcha' question
In a deadlock, threads are blocked and stuck — no progress and no CPU activity. In a livelock, threads
are actively responding to each other (e.g., both repeatedly backing off to avoid a collision) but still make no
real progress — classic analogy: two people in a hallway repeatedly stepping the same way to avoid each
other. Starvation is when a thread is perpetually denied access to a resource (e.g., always loses out to
higher-priority threads) even though the system overall is making progress.

Q14. Write a producer-consumer implementation. What are the two common ways to
do this in Java?
Tests: Coding round classic
Classic approach: a shared queue guarded by wait()/notifyAll() where the producer waits when the queue is
full and the consumer waits when it's empty. Modern approach: use a BlockingQueue (e.g.,
ArrayBlockingQueue/LinkedBlockingQueue), which handles all the waiting/notifying internally via
put()/take() — almost always preferred in real code.

BlockingQueue queue = new ArrayBlockingQueue<>(10);

// Producer
[Link](item); // blocks if full

// Consumer
Integer item = [Link](); // blocks if empty

4. Concurrent Collections

Q15. Explain a scenario where you'd use ConcurrentHashMap over a synchronized


HashMap.
Tests: Common concurrent-collections question
A high-read, moderate-write in-memory cache accessed by many threads (e.g., a lookup table of feature
flags or config). A synchronized HashMap (or [Link]) locks the entire map for every
operation, serializing all access even for reads. ConcurrentHashMap uses fine-grained internal locking
(bucket-level, since Java 8) so multiple threads can read and write different parts of the map concurrently,
giving much higher throughput under contention.

Q16. A HashMap used heavily in a multithreaded service occasionally causes an


infinite loop / high CPU during resize under concurrent writes. What's happening?
Tests: Classic legacy-code interview trap
Plain HashMap is not thread-safe. Concurrent structural modification (like a resize/rehash triggered by two
threads inserting at once) can corrupt the internal bucket linked list, famously causing an infinite loop in
Java 7's HashMap implementation. Fix: use ConcurrentHashMap, or wrap with
[Link]() and synchronize externally on iteration too.

Q17. What is CopyOnWriteArrayList, and when is it appropriate?


Tests: Concurrent collections
It creates a fresh copy of the underlying array on every write (add/remove), while reads never block and
never see a ConcurrentModificationException. Ideal for lists that are read far more often than written —
e.g., a list of event listeners or a rarely-changing configuration list read by many threads. Not suitable for
write-heavy lists — copying on every write is expensive.

5. Executor Framework & Thread Pools

Q18. Your Spring Boot service processes 1000 orders sequentially, causing high
latency. How would you parallelize this safely?
Tests: Practical, very commonly asked
Use an ExecutorService or [Link]() to submit tasks concurrently instead of a
manual loop of new Thread(). Size the thread pool based on workload type: CPU-bound work should use
roughly (number of CPU cores), while I/O-bound work (DB calls, HTTP calls) can use a larger pool since
threads spend most time waiting. Always handle exceptions per task (don't let one failure silently swallow
others), and avoid shared mutable state across the parallel tasks.

ExecutorService pool = [Link](10);


List> futures = [Link]()
.map(order -> [Link](() -> process(order), pool))
.collect([Link]());
[Link]([Link](new CompletableFuture[0])).join();

Q19. What is the Executor Framework, and why is it preferred over manually creating
threads?
Tests: Fundamentals
It decouples task submission from the mechanics of thread management. Manually creating a new Thread()
per task is expensive (thread creation overhead) and gives no control over concurrency limits, leading to
resource exhaustion under load. ExecutorService manages a reusable pool of worker threads, queues
excess tasks, and provides lifecycle control (shutdown, awaitTermination) and Future-based result
handling.

Q20. What are the common types of thread pools, and how do you pick one?
Tests: Fundamentals
FixedThreadPool — fixed number of threads, good for predictable, steady CPU-bound workloads.
CachedThreadPool — creates threads as needed and reuses idle ones, good for many short-lived, bursty
tasks but risky under sustained high load (unbounded growth). ScheduledThreadPool — for
delayed/periodic tasks (like a cron-style job). SingleThreadExecutor — guarantees sequential task
execution. In production, most teams define a custom ThreadPoolExecutor with an explicit bounded
queue and rejection policy rather than relying on the default Executors factory methods, to avoid
unbounded queue growth.

6. Advanced Concurrency Utilities

Q21. What is CompletableFuture, and how is it different from a plain Future?


Tests: Frequently asked at product companies
A plain Future only supports blocking get() to retrieve a result — no way to chain callbacks or combine
multiple async results. CompletableFuture supports a fluent, non-blocking pipeline: thenApply (transform
result), thenCompose (chain another async call), thenCombine (merge two independent futures), and
exception handling via exceptionally/handle — much closer to how async code is written in modern Java.

Q22. How would you combine the results of two independent async calls (e.g., fetch
user + fetch orders in parallel) and merge them once both complete?
Tests: Real-world async scenario
Run both calls as separate CompletableFutures so they execute concurrently, then combine using
thenCombine(), which waits for both and merges their results with a BiFunction.

CompletableFuture userFuture = [Link](() -> fetchUser(id));


CompletableFuture> ordersFuture = [Link](() -> fetchOrders(id));

CompletableFuture combined = [Link](


ordersFuture, (user, orders) -> new UserProfile(user, orders));

Q23. What is ThreadLocal, and give a real backend use case.


Tests: Frequently asked, especially at product companies
ThreadLocal gives each thread its own independent copy of a variable, isolated from other threads — no
synchronization needed since there's no sharing. Common backend use case: storing request-scoped
context such as the current logged-in user, a correlation/trace ID for logging (e.g., populating MDC in
SLF4J for distributed tracing), or a per-request database transaction handle.

Q24. What's a common pitfall with ThreadLocal in a thread-pool-based server (e.g.,


Tomcat), and how do you avoid it?
Tests: Real production bug — very commonly asked follow-up
Application servers reuse a fixed pool of worker threads across many requests. If a ThreadLocal value set
during one request isn't cleared, the next request handled by the same pooled thread can see stale
data from a previous, unrelated request — a subtle and dangerous bug (e.g., leaking one user's session
data to another). Fix: always call [Link]() in a finally block (or a servlet filter's finally clause)
at the end of request processing.

Q25. What is a CountDownLatch, and how is it different from a CyclicBarrier?


Tests: Concurrency utilities
CountDownLatch lets one or more threads wait until a set of operations being performed in other threads
completes — the counter only counts down and cannot be reset once it hits zero (one-time use).
CyclicBarrier makes a fixed number of threads wait for each other to reach a common barrier point, and
importantly can be reused for multiple rounds — useful for parallel computations split into phases.
Q26. What is a Semaphore, and give a scenario where you'd use one.
Tests: Concurrency utilities
A Semaphore maintains a set of permits; threads call acquire() to take a permit (blocking if none available)
and release() to return it — used to limit concurrent access to a resource. Real scenario: capping the
number of concurrent outbound calls to a rate-limited third-party API (e.g., allow at most 5 simultaneous
calls) to avoid overwhelming it or hitting its rate limit.

Semaphore semaphore = new Semaphore(5); // max 5 concurrent calls


[Link]();
try {
callExternalApi();
} finally {
[Link]();
}

Q27. What is the Fork/Join framework, and when would you use it over a regular
ExecutorService?
Tests: Advanced
ForkJoinPool is designed for divide-and-conquer workloads — recursively splitting a big task into smaller
subtasks (fork), processing them in parallel, and combining results (join). It uses work-stealing: idle
threads pull tasks from busy threads' queues, improving CPU utilization for uneven workloads. Best for
CPU-bound recursive algorithms (e.g., parallel merge sort, large array processing); parallelStream()
internally uses the common ForkJoinPool.

7. Java Memory Model & volatile

Q28. Two threads read a boolean flag to decide whether to keep running a loop, but
one thread's update to the flag from another thread is never seen — the loop never
stops. Why, and how do you fix it?
Tests: Extremely common JMM scenario question
Without volatile or synchronization, there is no happens-before guarantee between the writing and reading
thread — the reading thread may keep using a cached/stale value due to CPU caching and compiler/JIT
reordering optimizations. Fix: mark the flag volatile, which guarantees visibility of writes to all threads
immediately. Note: volatile alone doesn't make compound operations like increments atomic — for that, use
AtomicBoolean/AtomicInteger or synchronization.

private volatile boolean running = true;

public void stop() { running = false; } // visible to all threads immediately

Q29. What does the volatile keyword actually guarantee, and what does it NOT
guarantee?
Tests: Follow-up depth question
Guarantees: (1) Visibility — writes by one thread are immediately visible to others (no stale caching). (2)
Ordering — prevents instruction reordering around the volatile read/write (establishes happens-before).
Does NOT guarantee atomicity for compound operations — 'volatileCounter++' is still a read-modify-write
with a race condition even though the variable is volatile.

Q30. What is happens-before ordering in the Java Memory Model, and why does it
matter beyond just volatile?
Tests: Deeper theoretical question, product companies
Happens-before is the formal guarantee the JMM provides that if action A happens-before action B, then
A's effects (writes) are visible to B. It's established not just by volatile, but also by monitor locks
(synchronized), [Link]()/join(), and classes like CountDownLatch. Understanding this explains why,
for example, a thread reading a non-volatile field written inside a synchronized block by another thread is
still guaranteed to see the latest value — the lock acquisition/release itself establishes happens-before.

8. Real-World Scenario Questions

Q31. Your application throws OutOfMemoryError under heavy concurrent load, and
thread dumps show hundreds of threads stuck. What might be going wrong
architecturally?
Tests: Production debugging
Likely an unbounded thread pool (e.g., [Link]()) creating a new thread per
request without limits, or a bounded pool with an unbounded task queue causing memory to balloon under
backlog. Fix: use a properly sized, bounded ThreadPoolExecutor with a bounded queue and a sensible
rejection policy (e.g., CallerRunsPolicy to apply backpressure), and investigate whether downstream calls
(DB, external API) are the real bottleneck causing threads to pile up waiting.

Q32. A batch job needs to call an external API for 10,000 records, but calling it
sequentially takes hours, and calling all 10,000 in parallel crashes the external service.
How do you design this?
Tests: Real backend scenario, throttled concurrency
Use a bounded thread pool or a Semaphore to cap concurrency (e.g., 20 concurrent calls at a time),
combined with CompletableFuture for async orchestration. Optionally add rate limiting (token bucket) if the
external API enforces a requests-per-second limit, plus retry with exponential backoff for transient failures,
and circuit breaking (e.g., Resilience4j) if the external service starts failing consistently.

Q33. You need to cache the result of an expensive computation, but multiple
concurrent requests for the same missing key all trigger the expensive computation
simultaneously (cache stampede). How do you prevent this?
Tests: Concurrency + caching design question
Use [Link]() (which is atomic per key) to ensure only one thread computes
the value for a given key while others wait for that same computation, rather than all threads independently
recomputing. For distributed caches (Redis), use a short-lived lock key or a 'compute-once' pattern to
prevent stampede across multiple app instances.

ConcurrentHashMap cache = new ConcurrentHashMap<>();


Data value = [Link](key, k -> expensiveComputation(k));

9. Tricky Output-Based Questions

Q34. If a method is synchronized on 'this', can two threads still execute two
DIFFERENT synchronized methods on two DIFFERENT instances of the same class at
the same time?
Tests: Conceptual trap
Yes. The lock is per-object instance, not per-class (unless the method is static). Two threads operating on
two different instances acquire two different locks and can run concurrently without blocking each other — a
common misconception is that synchronized locks the whole class.

Q35. What happens if an exception is thrown inside a synchronized block — is the lock
released?
Tests: Trap question
Yes — the JVM guarantees the intrinsic lock is released automatically when the synchronized block exits,
whether normally or via an exception. This is one advantage synchronized has over manually managed
ReentrantLock, where you must remember to call unlock() in a finally block yourself, or the lock stays held.

Q36. A thread calls wait() without holding the lock on the object. What happens?
Tests: Trap question
It throws IllegalMonitorStateException at runtime. wait(), notify(), and notifyAll() must always be called
from within a block/method synchronized on the same object they're being called on.

Q37. Is 'i++' where i is a volatile int thread-safe? Why or why not?


Tests: Classic trick question
No. volatile only guarantees visibility and ordering, not atomicity. 'i++' is still a read-modify-write sequence of
three separate operations, so two threads can still interleave and lose an update, even though the variable
is volatile. For atomic increments, use AtomicInteger instead.

Tip: In interviews, always pair a definition with a short real example or code snippet — interviewers at product
companies weigh practical judgment (trade-offs, production pitfalls) far more heavily than textbook definitions
alone.

You might also like