Java Concurrency — Interview Ready Q&A
2. Can we make an Array volatile in Java?
Yes, but volatile applies to the array reference, not the individual elements.
private volatile int[] arr;
If one thread replaces the array:
arr = new int[]{1, 2, 3};
other threads will see the updated array reference.
But:
arr[0] = 10;
is not made thread-safe by volatile.
Interview line: "
volatileguarantees visibility of the array reference, not atomicity or visibility guarantees for individual array elements."
3. Can you write code for Double-Checked Locking Singleton?
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Why volatile?
Object creation involves multiple steps, and without volatile, instruction reordering could allow another thread to observe a partially constructed object.
Interview line: "We use double checking to avoid synchronization overhead after initialization, and
volatileguarantees safe publication."
4. What happens if the object you're synchronizing on is null?
Object lock = null;
synchronized (lock) {
// code
}
It throws:
NullPointerException
You cannot synchronize on null because null has no object monitor.
Interview line: "The JVM requires a valid object monitor, and
nulldoesn't have one."
5. Why doesn't ConcurrentHashMap Iterator throw ConcurrentModificationException?
ConcurrentHashMap iterators are designed for concurrent modifications.
They are weakly consistent:
- Don't throw
ConcurrentModificationException - Can reflect some modifications made during iteration
- Don't represent a fixed snapshot
- Continue safely while other threads modify the map
Example:
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
for (Integer key : map.keySet()) {
map.put(10, "value");
}
This doesn't fail with ConcurrentModificationException.
Interview line: "ConcurrentHashMap provides weakly consistent iterators that can safely operate while the map is being modified concurrently."
6. What is Busy Spin Waiting? What is the benefit?
Busy spinning means repeatedly checking a condition instead of putting the thread to sleep.
while (!condition) {
// keep checking
}
The thread continuously consumes CPU.
Benefit
Very low latency because there is no thread parking, wake-up, or context-switch overhead.
Useful when:
- Expected wait time is extremely short
- Low latency is more important than CPU efficiency
Interview line: "Busy spinning trades CPU consumption for very low latency."
7. If start() calls run(), why not call run() directly?
Actually, start() does not simply call run() on the current thread.
thread.start();
starts a new thread, and that thread executes run().
Whereas:
thread.run();
is just a normal method call and executes on the current thread.
Example
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
t.start(); // New thread
t.run(); // Current thread
Interview line: "
start()creates a new execution thread; directly callingrun()does not."
8. Why should we call wait() and notify() in a loop and not an if block?
Correct:
synchronized (lock) {
while (!condition) {
lock.wait();
}
// proceed
}
Not:
synchronized (lock) {
if (!condition) {
lock.wait();
}
// proceed
}
Why?
Because waking up does not guarantee that the condition is still true.
There can also be spurious wakeups.
After waking up, the thread should always re-check the condition.
Interview line: "We use
whilebecause waking up doesn't guarantee that the condition is true; the thread must re-check it."
Few More Questions
9. If asked to synchronize between 2 threads or 100 threads, which is harder and why?
100 threads is harder to reason about.
With 2 threads, there are relatively few possible execution orders.
With 100 threads, you have:
- Many possible interleavings
- More contention
- Race conditions
- Deadlocks
- Starvation
- Visibility problems
- More difficult debugging and testing
The synchronization primitive itself may be the same, but reasoning about correctness becomes much harder.
Interview line: "The difficulty comes from the huge number of possible thread interleavings and interactions."
10. Tell me three problems you usually face in a concurrent environment.
1. Race Condition
Multiple threads access shared state concurrently and produce an incorrect result.
Example:
count++;
is not atomic.
2. Deadlock
Two or more threads wait indefinitely for locks held by each other.
Thread A → Lock 1 → waits for Lock 2
Thread B → Lock 2 → waits for Lock 1
3. Visibility Problem
One thread updates a variable, but another thread doesn't immediately see the updated value.
Can be addressed using mechanisms such as:
volatilesynchronizedLock- Atomic classes
Interview line: "The three common problems are race conditions, deadlocks, and visibility issues."
11. What happens if you add a task to a Fixed Thread Pool and the worker queue is full?
There is an important distinction.
Executors.newFixedThreadPool()
ExecutorService executor =
Executors.newFixedThreadPool(10);
The default implementation uses an unbounded LinkedBlockingQueue.
Therefore, the queue normally doesn't become full.
Bounded ThreadPoolExecutor
For example:
ExecutorService executor =
new ThreadPoolExecutor(
10,
10,
0,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100)
);
If:
- All 10 workers are busy, and
- The queue contains 100 tasks
then the next task is rejected.
The behavior depends on the configured RejectedExecutionHandler.
For example:
new ThreadPoolExecutor.AbortPolicy()
throws:
RejectedExecutionException
Other policies include:
AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
Interview line: "If all workers are busy and a bounded queue is full, the task is handled by the configured
RejectedExecutionHandler."
12. What happens if an exception is thrown inside a Thread?
For a raw Thread:
Thread t = new Thread(() -> {
throw new RuntimeException("Something went wrong");
});
If the exception is uncaught:
- The thread terminates.
- The exception goes to the thread's
UncaughtExceptionHandler. - If no handler is configured, the default handler generally prints the stack trace.
Example:
t.setUncaughtExceptionHandler((thread, exception) -> {
System.out.println(
"Exception: " + exception.getMessage()
);
});
Important: ExecutorService
If you use:
Future<?> future = executor.submit(task);
the exception is captured by the Future.
It becomes visible when:
future.get();
is called.
You'll get:
ExecutionException
Interview line: "For a raw Thread, an uncaught exception terminates that thread and is handled by the UncaughtExceptionHandler. With ExecutorService, submitted-task exceptions can be captured in the Future."
🔥 30-Second Revision
| Question | Key Point |
|---|---|
volatile Array |
Reference is volatile; elements aren't |
| Double-Checked Locking |
volatile + double check + synchronized
|
Synchronize on null
|
NullPointerException |
ConcurrentHashMap Iterator |
Weakly consistent; no CME |
| Busy Spin | CPU usage for lower latency |
start() vs run()
|
start() → new thread; run() → current thread |
wait() in loop |
Re-check condition + handle spurious wakeups |
| 2 vs 100 threads | More interleavings and contention |
| Concurrency problems | Race condition, deadlock, visibility |
| Full thread-pool queue | Rejection handler |
| Thread exception | Thread terminates; uncaught handler handles it |
Top comments (0)