Series of Backend Interview Questions for Java Developers Part 2
Diving into backend interviews often feels like entering a detective novel: each question hides a mystery and you must reason, infer, and sometimes laugh quietly at the absurdity of race conditions. In this part of our series, we explore several ...

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
1. What does a thread dump contain, and how do you detect deadlocks from a stack trace?
"RUNNABLE", "BLOCKED", "WAITING", timestamps, priorities, daemon flags, and monitor ownership lines revealing which lock belongs to which thread.
Found one Java-level deadlock: because the JVM helpfully tattles. But beyond that friendly notice, reading thread stacks shows a circular dependency pattern — Thread A waits for a monitor owned by Thread B, while Thread B waits for a monitor owned by Thread A. In application logs, it looks almost comedic — two people politely holding a door for each other forever.
public class DeadlockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (LOCK_A) {
sleep(200);
synchronized (LOCK_B) {
System.out.println("Thread 1 acquired both locks");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
sleep(200);
synchronized (LOCK_A) {
System.out.println("Thread 2 acquired both locks");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
jstack
). The dump will reveal two threads holding different locks while waiting for the other. Analysing this teaches you that deadlocks are best prevented through lock ordering discipline or advanced constructs like ReentrantLock.tryLock. Imagine this like dancing — everyone should know whose turn it is, or the choreography collapses.
2. How does the Java Memory Model explain “visibility” phenomena?
volatile keyword, synchronized operations, locks, and atomic classes inject happens-before relationships — logical bridges ensuring that when a thread writes something and another reads, that read observes the latest committed value rather than stale history. Without JMM guarantees, multi-core systems would behave like rebellious teenagers ignoring instructions.
public class VisibilityIssue {
private static boolean running = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (running) {
// busy loop
}
System.out.println("Stopped");
});
t.start();
sleep(1000);
running = false; // main thread updates flag
System.out.println("Main updated flag");
}
private static void sleep(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { }
}
}
running value. Marking running as volatile forces memory visibility. The JMM provides this guarantee — a write to volatile establishes happens-before ordering with subsequent reads. Understanding this is non-negotiable when writing lock-free algorithms or tuning concurrency frameworks.
3. When should you use CopyOnWriteArrayList, and when should you absolutely not?
CopyOnWriteArrayList<string> listeners = new CopyOnWriteArrayList<>();
public void register(String l) {
listeners.add(l); // rare
}
public void notifyAllListeners(String event) {
for (String l : listeners) {
System.out.println("Notifying " + l + " with " + event);
}
}
</string>
4. What are the strengths and trade-offs of CompletableFuture?
CompletableFuture<integer> task =
CompletableFuture.supplyAsync(() -> compute(5))
.thenApply(result -> result * 2)
.thenCompose(x -> fetchMoreAsync(x))
.exceptionally(ex -> -1);
System.out.println(task.join());
5. What is the difference between parallel streams and ExecutorService?
.parallel() and tasks suddenly dance across threads. But like most magic tricks, control is limited. They borrow threads from ForkJoinPool.commonPool, hide scheduling and lifetime decisions, and aren’t tuned for precise throughput or heavy I/O coordination.
// parallel stream
List<integer> nums = List.of(1,2,3,4,5);
nums.parallelStream()
.map(this::compute)
.forEach(System.out::println);
// executor service
ExecutorService pool = Executors.newFixedThreadPool(3);
for (Integer n : nums) {
pool.submit(() -> compute(n));
}
pool.shutdown();
Read more at : Series of Backend Interview Questions for Java Developers Part 2





