Skip to main content

Command Palette

Search for a command to run...

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 ...

Published
6 min read
Series of Backend Interview Questions for Java Developers Part 2
T

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?

A thread dump is essentially an MRI scan of the JVM’s brain taken at a specific moment in execution. It reveals every thread, its state, call stack, whether it holds or waits for locks, and hints about resource contention. Picture a crime investigation board where strings connect suspects — thread dumps help you trace who is blocking whom. You’ll see entries like "RUNNABLE", "BLOCKED", "WAITING", timestamps, priorities, daemon flags, and monitor ownership lines revealing which lock belongs to which thread.

To identify deadlocks, your eyes immediately search for the section labeled 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.

Here is a tiny Java program engineered to commit deadlock, useful for teaching yourself to read dumps:

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(); }
}
}

Run this and capture a thread dump (e.g., 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?

Visibility is the eerie moment where one thread modifies a shared variable, yet another thread blissfully remains unaware — like two coworkers sitting back-to-back but never turning around. The Java Memory Model (JMM) explains this through caching rules: each thread may maintain its own worker-copy of data, and without specific guarantees, writes from one thread may never flush into main memory where others read.

The 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.

Example:

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) { }
}
}

Sometimes this program never prints “Stopped” because the worker thread never sees the updated 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 is the philosophical Zen-monk of Java collections — serene, immutable under change, preferring cloning itself rather than mutating. Each write operation creates a brand-new array copy, ensuring iteration always sees a consistent snapshot. This is delightful for low-write/high-read patterns like observer lists, routing tables, or configuration caches where stability outweighs mutation cost.

But it becomes the villain when the write frequency increases — imagine rewriting entire novels every time you add a punctuation mark. In high-write use cases, this collection thrashes memory, amplifies GC pressure, and causes unpredictable pauses. If multiple writers hammer it, hilarity turns to horror — even better, performance flames out spectacularly. Use it for event listener tracking, never for chat message histories or queues.

Example illustrating good usage:

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>

Notice that reads dominate writes, perfect for this data structure’s superpower.

4. What are the strengths and trade-offs of CompletableFuture?

CompletableFuture is Java’s version of caffeine-infused concurrency — chaining, pipelining, composing, reacting, and modelling async flows elegantly. Unlike raw futures, CompletableFuture lets you sequence computations, attach callbacks, merge responses, declare timeouts, and orchestrate asynchronous pipelines like Lego bricks.

Instead of waiting for a call to complete, work continues, and when results land, continuation functions fire like fireworks. This leads to non-blocking, highly expressive concurrency patterns. But beware — debugging async chains may feel like untangling spaghetti with chopsticks. Thread explosion, silent handler loss, and complex error handling lurk if you stack operations without thought.

Illustration in code:

CompletableFuture<integer> task =
CompletableFuture.supplyAsync(() -> compute(5))
.thenApply(result -> result * 2)
.thenCompose(x -> fetchMoreAsync(x))
.exceptionally(ex -> -1);

System.out.println(task.join());

Here, each stage transforms results automatically — reminiscent of mini-data pipelines. This fluency helps when building microservices, APIs, or I/O heavy flows. Reflection point: if everything is synchronous and trivial, CompletableFuture adds unnecessary mental overhead — use it when concurrency is meaningful.

5. What is the difference between parallel streams and ExecutorService?

Parallel streams feel magical — you sprinkle .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.

ExecutorService, by contrast, is the serious engineer — configurable thread pools, lifecycle control, rejection policies, queue behaviours, and task monitoring. You build it like you’d design a production assembly line rather than waving a wand hoping cars assemble themselves.

Example contrast:

// 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();

Parallel streams excel in CPU-bound pure functional workloads where splitting data evenly works beautifully. They fail when tasks block, involve I/O, or require custom scheduling. ExecutorService wins in backpressure, connection handling, retry policies, and practically every system demanding reliability — the difference between a toy model train and an industrial railroad.

If you found this chapter insightful or want to dive deeper — perhaps profiling GC pauses, lock contention debugging, or reactive frameworks — just drop your questions in the comments below. I’d love to expand this series based on your curiosity!

Read more at : Series of Backend Interview Questions for Java Developers Part 2

More from this blog

T

tuanh.net

540 posts

Are you ready to elevate your Java, OOP, Spring, and DevOps skills? Look no further!