Java Concurrency – Interview & Real-World Scenarios
This final part in our Java Concurrency series focuses on interview-specific pitfalls and real-world concurrency scenarios. You’ll learn how to avoid traps like deadlocks, design producer–consumer pipelines, and compare concurrency models.
1. Common Interview Pitfalls
Singleton and Concurrency
A common interview question: “How do you make a Singleton thread-safe in Java?”
- Incorrect (not thread-safe):
java
public class BadSingleton {
private static BadSingleton instance;
public static BadSingleton getInstance() {
if (instance == null) {
instance = new BadSingleton();
}
return instance;
}
}
- Thread-safe (synchronized method):
java
public class SafeSingleton {
private static SafeSingleton instance;
public static synchronized SafeSingleton getInstance() {
if (instance == null) {
instance = new SafeSingleton();
}
return instance;
}
}
- Better (double-checked locking with volatile):
java
public class DCLSingleton {
private static volatile DCLSingleton instance;
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
Deadlock Avoidance
- Scenario: Two threads hold locks in opposite order.
- Fix: Always acquire locks in a consistent global order or use
tryLock()
with timeout.
Other Interview Traps
- Misusing
volatile
to guarantee atomicity (it doesn’t). - Assuming
HashMap
is thread-safe (it’s not). - Forgetting to shut down an
ExecutorService
.
2. Designing a Producer–Consumer Pipeline
A classic concurrency problem, common in interviews and real systems.
Example with BlockingQueue
java
import java.util.concurrent.*;
class ProducerConsumer {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
Runnable producer = () -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
System.out.println("Produced " + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
Runnable consumer = () -> {
try {
while (true) {
int item = queue.take();
System.out.println("Consumed " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
new Thread(producer).start();
new Thread(consumer).start();
}
}
- Why BlockingQueue?
- Handles synchronization internally.
- Blocks producers if full, consumers if empty.
- Avoids manual
wait()
/notify()
pitfalls.
3. Comparing Concurrency Models
Threads (Low-Level Model)
- Explicitly create and manage threads.
- Full control but more error-prone.
- Good for fine-grained concurrency, but harder to scale.
Async/Callback Model
- Uses event loops and futures.
- Example:
CompletableFuture
in Java.
java
CompletableFuture.supplyAsync(() -> "data")
.thenApply(String::toUpperCase)
.thenAccept(System.out::println);
- Great for I/O-bound tasks.
- Drawback: Callback hell if not structured well.
Actor Model
- Each actor encapsulates state and communicates via messages.
- Popularized by Akka (Java/Scala).
- Scales horizontally across cores/nodes.
- Great for highly concurrent, distributed systems.
- Tradeoff: Learning curve, debugging message-passing.
4. Best Practices for Interviews & Real Systems
- Always discuss tradeoffs (e.g.,
synchronized
vsLock
). - Mention scalability and fairness in locks.
- For pipelines, suggest backpressure handling (bounded queues).
- Prefer immutable objects to reduce synchronization.
- Highlight higher-level abstractions (
ExecutorService
,CompletableFuture
,Streams
).
5. Quick Interview Checklist
- Explain thread safety in Singleton.
- Avoid deadlocks → consistent lock ordering.
- Know BlockingQueue producer–consumer.
- Compare Thread vs Async vs Actor.
- Show knowledge of CompletableFuture for async flows.
Conclusion
Concurrency is not just about writing threads — it’s about designing safe, scalable systems. For interviews, focus on correctness, clarity, and tradeoffs. In real-world systems, use higher-level abstractions and proven concurrency models to reduce complexity.