Skip to content
Home Interview Java Multithreading Deadlock — Payment Batch Hang

Java Multithreading Deadlock — Payment Batch Hang

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Java Interview → Topic 4 of 6
Payment batch hangs after 500 transfers due to circular lock acquisition.
🔥 Advanced — solid Interview foundation required
In this tutorial, you'll learn
Payment batch hangs after 500 transfers due to circular lock acquisition.
  • Concurrency is about managing shared mutable state; if state isn't shared or isn't mutable, it's thread-safe by default.
  • Volatile solves visibility but not atomicity; Atomic classes use hardware-level CAS for lock-free thread safety.
  • Always favor high-level concurrency utilities (java.util.concurrent) like ExecutorService or CountDownLatch over raw Thread objects.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Multithreading lets multiple tasks run concurrently on separate CPU cores
  • Core concept: shared mutable state must be synchronized to prevent race conditions
  • volatile guarantees visibility, not atomicity; Atomic* classes use CAS for lock-free atomics
  • synchronized uses monitor locks with bias→lightweight→heavyweight escalation
  • Performance insight: lock contention adds ~1-10µs per acquisition under low contention, spikes to ms under high contention
  • Production insight: thread dumps reveal deadlocks, but silent liveness failures (starvation) are harder to catch
  • Biggest mistake: assuming volatile makes read-modify-write operations thread-safe
🚨 START HERE

Quick Multithreading Debugging Cheat Sheet

Commands and checks to diagnose the most frequent concurrency failures in under 5 minutes.
🟡

Threads stuck in BLOCKED state

Immediate ActionCheck thread dump for lock ownership chains.
Commands
jstack <pid> | grep -A 30 'BLOCKED'
jcmd <pid> Thread.print -l
Fix NowIdentify circular waiting — restart the application with a consistent lock ordering fix.
🟡

NullPointerException in concurrent code

Immediate ActionCheck if the variable is declared volatile or accessed within synchronized.
Commands
javap -c -p <classname> | grep 'getstatic\|putstatic'
jstack <pid> | grep 'RUNNABLE'
Fix NowAdd volatile keyword or synchronize the access.
🟡

Race condition causing incorrect counts

Immediate ActionInspect the increment operation — volatile count++ is not atomic.
Commands
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=print,*YourClass.methodName
Use jstack to confirm multiple threads are executing the same block.
Fix NowReplace with AtomicInteger or synchronized block.
🟡

Application deadlocks under load

Immediate ActionUse jconsole or VisualVM to detect deadlocks automatically.
Commands
jcmd <pid> Thread.print -l
kill -3 <pid> > threaddump.txt
Fix NowRedesign lock ordering — if possible, use java.util.concurrent locks with tryLock.
Production Incident

Deadlock in Payment Batch Processor Took Down Production

A scheduled batch job that transferred funds between accounts deadlocked every few hours. Two threads each held one lock and waited for the other — classic deadly embrace.
SymptomPayment batch job would occasionally hang after processing ~500 transfers. No error logs. Threads remained BLOCKED indefinitely. Manual intervention (kill -9) was the only recovery.
AssumptionJunior dev assumed using synchronized on individual transfer methods was sufficient because each method was thread-safe in isolation.
Root causeThread A acquired lock on Account X then tried to acquire lock on Account Y. Simultaneously, Thread B acquired lock on Account Y then tried to acquire lock on Account X. Both threads blocked forever because they held locks the other needed.
FixImplemented a consistent lock ordering based on account ID (always lock the smaller ID first). Also added a timeout using tryLock with a fallback rollback.
Key Lesson
Always acquire locks in a global consistent order to avoid deadlocks.Use tryLock with a timeout as a safety net — never rely on indefinite blocking.Monitor thread dumps periodically in production to detect blocking threads early.
Production Debug Guide

Symptom → Action guide for the most common concurrency bugs

Application hangs or becomes unresponsiveTake thread dump (jstack <pid> or kill -3 <pid>). Look for threads in BLOCKED state waiting on the same monitor. Also check for threads in RUNNABLE that are stuck in an infinite loop.
Data corruption or inconsistent stateEnable -XX:+PrintConcurrentLocks or use jcmd <pid> Thread.print -l. Check for missing synchronized blocks around shared mutable state. Often the problem is a field that is not volatile or not synchronized.
Unexpected InterruptedExceptionVerify all catch blocks for InterruptedException call Thread.currentThread().interrupt() to restore the interrupt flag. Failing to do so kills thread shutdown signals.
High CPU usage but no progressLook for busy-wait loops (while(!ready){}). Replace with wait/notify or CountDownLatch. Use jstack to find threads consuming CPU.

Multithreading questions separate senior Java developers from juniors faster than almost anything else in an interview. It's not enough to know that synchronized exists — interviewers at companies like Amazon, Google, and Goldman Sachs want to know what happens inside the JVM when two threads collide on a shared object, why volatile doesn't make compound operations atomic, and how the Java Memory Model actually defines 'visibility'. These are the questions that decide offers.

The real problem multithreading solves is utilising multi-core hardware. Modern servers have 32, 64, even 128 cores sitting idle if your application is single-threaded. But concurrency introduces an entirely new class of bugs — race conditions, deadlocks, liveness failures, and memory visibility errors — that are notoriously hard to reproduce and even harder to debug in production. A solid mental model is your only real defence.

By the end of this article you'll be able to answer the top Java multithreading interview questions with the depth and precision that impresses senior engineers. You'll understand the Java Memory Model, the monitor mechanism behind synchronized, the happens-before guarantee, the difference between Callable and Runnable at the implementation level, and the patterns that prevent deadlock. You'll walk into that interview room ready to discuss internals, not just syntax.

The Java Memory Model (JMM) & The Visibility Problem

In a multi-core environment, threads don't just talk to main memory; they have local CPU caches. This creates a visibility problem: Thread A might update a variable in its cache, but Thread B on another core still sees the old value in main memory. The JMM defines the 'Happens-Before' relationship, ensuring that memory writes by one specific statement are visible to another specific statement. Without proper synchronization or the volatile keyword, the JVM is actually allowed to reorder your code for optimization, which can lead to disastrous results in concurrent execution.

Here's the thing: most engineers think volatile makes all reads see the latest write. That's true for simple reads, but for compound operations like count++, you still get a race. Volatile only guarantees visibility, not atomicity. If you're doing read-modify-write, you need AtomicInteger or synchronized.

The JMM also defines the happens-before rule for thread start and join: calling Thread.start() happens-before any action in the started thread. Similarly, all actions in a thread happen-before another thread successfully returns from Thread.join(). These rules let you safely share initialization data without explicit synchronization.

io.thecodeforge.concurrency.VisibilityDemo.java · JAVA
12345678910111213141516171819202122
package io.thecodeforge.concurrency;

public class VisibilityDemo {
    // Without volatile, the 'running' thread might never see the update from main
    private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (running) {
                // The CPU might optimize this into an infinite loop without volatile
            }
            System.out.println("Worker thread stopped safely.");
        });

        worker.start();
        Thread.sleep(1000);
        
        System.out.println("Requesting stop...");
        running = false;
        worker.join();
    }
}
▶ Output
Requesting stop...
Worker thread stopped safely.
🔥Forge Tip: Volatile vs Atomic
Remember: volatile only guarantees visibility, NOT atomicity. If you are doing a compound operation like count++, volatile is not enough because the read-modify-write cycle can still be interrupted. For that, you need AtomicInteger or synchronized.
📊 Production Insight
In production, visibility bugs often manifest as 'it works on my machine' — the developer's single-core laptop doesn't reproduce the issue.
Use jstack to check if threads are spinning in loops waiting on non-volatile flags.
Rule: if a flag is written by one thread and read by another, mark it volatile or use AtomicBoolean.
🎯 Key Takeaway
Visibility doesn't come free.
Volatile fixes one thread seeing stale data, but compound ops still need atomicity.
If you're sharing a flag between threads, mark it volatile — don't rely on accidental synchronization.
When to Use Volatile vs Synchronized vs Atomic
IfSingle variable, only reads/writes (no compound ops)
UseUse volatile — simplest and fastest
IfSingle variable with read-modify-write (e.g., counter)
UseUse AtomicInteger/Long — lock-free via CAS
IfMultiple variables that must be updated atomically together
UseUse synchronized block or ReentrantLock
IfYou need to block threads waiting for a condition
UseUse wait/notify or higher-level synchronizers like CountDownLatch

Locks, Monitors, and Synchronized Internals

Every object in Java is associated with a 'Monitor'. When a thread enters a synchronized block, it must acquire the lock on that monitor. If the lock is held, the thread enters a BLOCKED state. Under the hood, the JVM optimizes this using 'Biased Locking' (now mostly deprecated in newer JDKs), 'Lightweight Locking', and finally 'Heavyweight Locking' involving OS-level mutexes. Understanding this escalation helps you write code that avoids unnecessary lock contention.

But here's the reality: synchronized is not as expensive as many junior devs fear. For uncontended locks, the JIT can eliminate the lock entirely (lock elision). Contended locks are the problem — they cause thread context switches that kill throughput. That's why ReentrantLock with tryLock can be a better choice under high contention: it avoids the OS mutex path if the lock is quickly available.

Wait/notify must always be called within a synchronized context because they are based on the monitor. When you call wait(), the thread releases the monitor and goes to WAITING state. When notify() is called, it wakes one thread, which must re-acquire the monitor before proceeding. This mechanism is fundamental to producer-consumer patterns.

io.thecodeforge.concurrency.DeadlockAvoidance.java · JAVA
12345678910111213141516
package io.thecodeforge.concurrency;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DeadlockAvoidance {
    private final Lock lockA = new ReentrantLock();
    private final Lock lockB = new ReentrantLock();

    public void safeTransfer() {
        try {
            // TryLock prevents the 'deadly embrace' where two threads wait forever
            if (lockA.tryLock(50, TimeUnit.MILLISECONDS)) {\n                try {\n                    if (lockB.tryLock(50, TimeUnit.MILLISECONDS)) {\n                        try {\n                            System.out.println(\"Securely accessed both resources.\");\n                        } finally {\n                            lockB.unlock();\n                        }\n                    }\n                } finally {\n                    lockA.unlock();\n                }\n            }\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n}",
        "output": "Securely accessed both resources."
      }

Thread Lifecycle and States: From NEW to TERMINATED

A Java thread goes through six well-defined states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. Understanding these states is critical for debugging production issues. When you take a thread dump, every thread's state tells a story.

NEW means the thread object exists but start() hasn't been called yet. RUNNABLE means it's executing or ready to execute (the JVM doesn't distinguish between running and runnable). BLOCKED means it's waiting for a monitor lock. WAITING means it entered via Object.wait(), Thread.join(), or LockSupport.park(). TIMED_WAITING is similar but with a timeout.

The mistake many make is thinking that a thread in RUNNABLE is actively using CPU. It might be stuck in a tight loop waiting for a flag that never changes — that's a liveness failure disguised as RUNNABLE. Always look at the stack trace, not just the state.

io.thecodeforge.concurrency.ThreadLifecycle.java · JAVA
1234567891011121314151617181920
package io.thecodeforge.concurrency;

public class ThreadLifecycle {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(5000);  // TIMED_WAITING
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        System.out.println("After creation: " + t.getState()); // NEW
        t.start();
        System.out.println("After start: " + t.getState()); // RUNNABLE
        Thread.sleep(100);
        System.out.println("During sleep: " + t.getState()); // TIMED_WAITING
        t.join();
        System.out.println("After join: " + t.getState()); // TERMINATED
    }
}
▶ Output
After creation: NEW
After start: RUNNABLE
During sleep: TIMED_WAITING
After join: TERMINATED
Mental Model
Thread State Mental Model
Think of thread states as a city subway map: each state is a station, and transitions are the tracks.
  • NEW: train at depot, not yet on tracks
  • RUNNABLE: moving or waiting for a slot on the CPU track
  • BLOCKED: waiting at a gate for another train to leave the station (synchronized lock)
  • WAITING: train parked in a siding waiting for a signal (wait/join/park)
  • TIMED_WAITING: parked with a timer — will automatically resume
  • TERMINATED: arrived at final destination, removed from service
📊 Production Insight
Threads in WAITING or TIMED_WAITING are not a problem per se — they're expected. But an excessive number of BLOCKED threads indicates lock contention.
In production, configure your thread pool size to avoid over-subscription. Too many threads cause more context switching than useful work.
Rule: if you see threads lingering in BLOCKED for seconds, you have a design problem — not a configuration problem.
🎯 Key Takeaway
Thread dump gives you state, but the stack trace gives you the story.
BLOCKED ≠ always bad, but sustained blocking always is.
RUNNABLE doesn't mean running — it could be a busy-wait disaster.
Interpreting Thread States in Dumps
IfMany threads in BLOCKED on the same monitor
UseLock contention — investigate the synchronized block for excessive duration
IfOne thread in BLOCKED, others waiting for it
UsePossible deadlock — look for circular lock dependencies
IfThread in RUNNABLE but no progress (infinite loop)
UseMissing volatile or broken wait condition — check the loop logic
IfAll threads idle (WAITING) but system not responding
UseStarvation or missing notify signals — check producer-consumer design

Deadlock Prevention and Detection: The Patterns That Save Your App

Deadlock is the most feared concurrency bug because it brings the system to a complete halt with no error message. It happens when two or more threads hold locks and wait indefinitely for locks held by each other. The classic example: Thread1 locks A, then tries B; Thread2 locks B, then tries A.

The Java approach to deadlock detection is via thread dumps — jstack or jcmd will automatically detect cycles and report 'Found one Java-level deadlock'. But detection is reactive. Prevention requires consistent lock ordering or using higher-level abstractions that avoid nested locks.

The real fix that senior engineers use is to minimize the number of locks held simultaneously. If you must hold multiple locks, always acquire them in the same order across all code paths. Also consider using tryLock with backoff — if you can't acquire all locks within a timeout, release everything and retry. This makes deadlock impossible because locks are never held forever waiting for another lock.

io.thecodeforge.concurrency.DeadlockDetector.java · JAVA
12345678910111213141516171819202122232425262728293031
package io.thecodeforge.concurrency;

public class DeadlockDetector {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread1: acquired lock1");
                try { Thread.sleep(50); } catch (InterruptedException e) {}
                synchronized (lock2) {
                    System.out.println("Thread1: acquired lock2");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread2: acquired lock2");
                try { Thread.sleep(50); } catch (InterruptedException e) {}
                synchronized (lock1) {
                    System.out.println("Thread2: acquired lock1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}
▶ Output
Thread1: acquired lock1
Thread2: acquired lock2
(and then hangs — deadlock detected by jstack)
⚠ Forge Warning: Deadlock vs Livelock
Deadlock: threads wait forever. Livelock: threads are active but make no progress (e.g., two polite threads repeatedly release and retry). Livelock can be harder to detect because threads are RUNNABLE, not BLOCKED. Use thread dumps to see repetitive stack patterns.
📊 Production Insight
Deadlock rarely happens during testing because timing is hard to reproduce. It manifests under specific load patterns.
Use tryLock with a timeout and a fallback: if you can't get all locks, back off and retry.
As a defensive measure, enable ThreadMXBean.findDeadlockedThreads() and alert if it returns non-empty.
Rule: a lock ordering policy is the only reliable prevention — document it in team coding standards.
🎯 Key Takeaway
Deadlock is a design bug, not a runtime glitch.
Consistent lock ordering is the only 100% prevention.
tryLock with timeout makes deadlock impossible — it's your safety net.
Deadlock Prevention Strategies
IfAlways need multiple locks
UseEnforce global lock ordering (e.g., by resource ID)
IfCan tolerate retries
UseUse ReentrantLock.tryLock() with timeout and rollback
IfOnly one lock needed at a time
UseNo deadlock possible — keep it simple
IfLegacy code with unknown ordering
UseAdd monitoring: log lock acquisition order during development, detect violations

Concurrency Utilities: From CountDownLatch to CompletableFuture

Raw threads and synchronized are the assembly language of concurrency. The java.util.concurrent package provides higher-level building blocks that handle common patterns safely and efficiently.

CountDownLatch lets one or more threads wait until a set of operations completes. CyclicBarrier lets a set of threads wait for each other to reach a common barrier point. Semaphore controls access to a pool of resources. Exchanger lets two threads exchange objects at a synchronization point.

But the workhorse in modern Java is CompletableFuture. It chains asynchronous tasks with thenApply, thenCompose, and exceptionally. It provides a declarative way to build async pipelines without nesting callbacks. When used with a ForkJoinPool, it can automatically parallelize independent stages.

The key production insight: CompletableFuture uses a default ForkJoinPool that is sized to the number of CPU cores. For I/O-bound tasks, this can starve CPU-bound work. Always supply a custom executor for I/O-heavy operations.

io.thecodeforge.concurrency.AsyncPipeline.java · JAVA
1234567891011121314151617181920212223242526272829303132
package io.thecodeforge.concurrency;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsyncPipeline {
    private static final ExecutorService ioExecutor = Executors.newFixedThreadPool(20);

    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> fetchUser(1), ioExecutor)
            .thenApplyAsync(user -> enrichProfile(user), ioExecutor)
            .thenAccept(profile -> sendNotification(profile))
            .exceptionally(e -> {
                System.err.println("Failed: " + e.getMessage());
                return null;
            });
    }

    private static String fetchUser(int id) {
        // Simulate IO call
        return "User_" + id;
    }

    private static String enrichProfile(String user) {
        return user + "_enriched";
    }

    private static void sendNotification(String profile) {
        System.out.println("Sent notification for " + profile);
    }
}
▶ Output
Sent notification for User_1_enriched
💡Forge Tip: Custom Executor for I/O
CompletableFuture.supplyAsync() uses ForkJoinPool.commonPool() by default. That pool is sized to number of CPU cores. For I/O operations, use a custom thread pool with more threads to avoid starving CPU-bound tasks.
📊 Production Insight
Using the default ForkJoinPool for I/O tasks is a common production mistake. It leads to thread starvation because I/O threads block waiting for responses while CPU cores sit idle.
Also, be careful with CompletableFuture.get() — it blocks the calling thread indefinitely. Always use a timeout: get(2, TimeUnit.SECONDS).
Rule: separate CPU-bound and I/O-bound thread pools. Use a larger pool for I/O and keep the ForkJoinPool for CPU-bound computations.
🎯 Key Takeaway
java.util.concurrent is your shield against most raw thread bugs.
CompletableFuture is powerful but requires explicit executor management.
CountDownLatch + CompletableFuture handle 80% of coordination patterns.
Choosing the Right Concurrency Utility
IfOne thread must wait for several tasks to complete
UseUse CountDownLatch or CompletableFuture.allOf()
IfMultiple threads must wait for each other at a point
UseUse CyclicBarrier
IfControl access to a fixed number of resources (e.g., database connections)
UseUse Semaphore
IfAsynchronous pipeline with chained operations
UseUse CompletableFuture with a custom executor for I/O
🗂 synchronized vs ReentrantLock
Choose the right locking mechanism for your context
FeaturesynchronizedReentrantLock
MechanismImplicit (Keyword)Explicit (API Class)
FairnessAlways UnfairOptional Fairness
FlexibilityBlock-structured onlyCan lock in one method, unlock in another
InterruptibilityNo (Thread stays blocked)Yes (via lockInterruptibly)
PerformanceExtremely optimized by JITBetter under high contention

🎯 Key Takeaways

  • Concurrency is about managing shared mutable state; if state isn't shared or isn't mutable, it's thread-safe by default.
  • Volatile solves visibility but not atomicity; Atomic classes use hardware-level CAS for lock-free thread safety.
  • Always favor high-level concurrency utilities (java.util.concurrent) like ExecutorService or CountDownLatch over raw Thread objects.
  • The Java Memory Model allows for compiler reordering; synchronization primitives act as 'memory barriers' to prevent this.
  • Thread dumps are your best friend for debugging concurrency issues. Learn to read BLOCKED vs WAITING vs RUNNABLE stacks.
  • Deadlock is prevented by consistent lock ordering — document the order and enforce it in code reviews.

⚠ Common Mistakes to Avoid

    Using volatile for counters (e.g., volatile int count++; is NOT thread-safe)
    Symptom

    Counter ends up with incorrect values under high concurrency — misses increments due to lost updates.

    Fix

    Use AtomicInteger for counters or wrap the increment in a synchronized block.

    Not releasing locks in a 'finally' block, leading to permanent resource starvation
    Symptom

    After an exception is thrown, the lock is never released. Other threads block indefinitely.

    Fix

    Always use try-finally (or try-with-resources for ReentrantLock) to ensure unlock() is called.

    Calling Thread.stop(), Thread.suspend(), or Thread.resume() — these are deprecated and dangerous
    Symptom

    Thread.stop() can leave objects in an inconsistent state because it releases all locks abruptly.

    Fix

    Use a volatile flag to signal the thread to stop gracefully, and handle InterruptedException properly.

    Ignoring the InterruptedException, which breaks the thread's ability to shut down gracefully
    Symptom

    Thread cannot be cancelled — it keeps running even when interrupted.

    Fix

    Always call Thread.currentThread().interrupt() in the catch block to restore the interrupt flag, then either propagate or abort.

    Assuming synchronized on a method makes the whole class thread-safe
    Symptom

    Race conditions appear when one method is synchronized but another accessor is not, or when state is shared across objects.

    Fix

    Ensure all methods that read or write shared mutable state are synchronized on the same lock. Consider using atomic classes or immutable objects.

    Using synchronized on a String literal or a boxed primitive
    Symptom

    Strings from the constant pool or small integers from the cache are shared across the JVM — two unrelated pieces of code can block each other.

    Fix

    Create a dedicated Object lock per class or use private final Object lock = new Object();

Interview Questions on This Topic

  • QHow does the 'Happens-Before' principle apply to a thread starting versus a thread joining, and how does it guarantee memory visibility?SeniorReveal
    The JMM defines that when a thread calls Thread.start(), all memory writes made by the calling thread before the start() call are guaranteed to be visible to the started thread. Similarly, when a thread calls Thread.join() and successfully returns, all memory writes made by the joined thread are guaranteed to be visible to the calling thread. This means you can safely share initialization data between threads without explicit synchronization if you ensure the writes happen before start() is called. However, this only applies to the initial data — subsequent shared state still needs synchronization.
  • QExplain the 'Double-Checked Locking' pattern for Singletons. Why was it broken in Java 1.4 and how did the volatile keyword fix it in Java 5?SeniorReveal
    Double-checked locking attempts to lazily initialize a singleton with minimal synchronization: check if instance is null (unsynchronized), if so synchronize, then check again inside the sync block, then create instance. In Java 1.4 and earlier, the JMM allowed the constructor's writes to be reordered before the assignment to the shared field. This meant a thread could see the instance as non-null but the object was not fully constructed. Java 5 fixed this by making the volatile keyword establish a happens-before relationship: reading a volatile after a write guarantees visibility of all preceding writes. By declaring the instance field volatile, the reordering is prevented and the pattern works correctly.
  • QCompare 'Optimistic Locking' using CAS (Compare-And-Swap) in Atomic classes versus 'Pessimistic Locking' in synchronized blocks. In which scenario would CAS perform worse?SeniorReveal
    Optimistic locking assumes low contention and retries on collision, using hardware CAS instructions. It scales well under low to moderate contention because there's no thread blocking. Pessimistic locking assumes high contention and blocks threads, causing context switches. CAS performs worse under very high contention because threads spin repeatedly, wasting CPU cycles. In such cases, synchronized or ReentrantLock with backoff can be better because they let threads sleep and reduce cache line bouncing. Also, CAS is typically faster for single-variable updates but cannot handle multiple variables atomically without coordination (e.g., using AtomicReference for compound objects).
  • QWhat is the difference between 'yielding', 'sleeping', and 'waiting' in terms of CPU usage and monitor ownership?Mid-levelReveal
    Thread.yield() is a hint to the scheduler that the current thread is willing to pause its execution. It might not do anything, and the thread remains RUNNABLE. Thread.sleep() causes the thread to enter TIMED_WAITING for a specified duration. It does not release any monitors. Object.wait() causes the thread to release the monitor and enter WAITING until another thread calls notify()/notifyAll() on the same object. This is the only one that releases locks. Sleep and yield keep locks held — a common mistake is to call sleep inside a synchronized block thinking it will let other threads proceed, but it doesn't.
  • QHow does a ForkJoinPool differ from a standard ThreadPoolExecutor? When would you use each?SeniorReveal
    ForkJoinPool specializes in work-stealing: idle threads steal tasks from busy threads' queues, improving parallelism for divide-and-conquer algorithms. It uses a work queue per thread, and tasks can recursively fork subtasks. ThreadPoolExecutor uses a shared blocking queue and is better for independent tasks. Use ForkJoinPool for recursive parallel tasks (e.g., computation-heavy algorithms). Use ThreadPoolExecutor for I/O-bound or heterogeneous tasks. CompletableFuture uses the common ForkJoinPool by default — that's why custom executors are needed for I/O.

Frequently Asked Questions

Why is it said that 'Wait/Notify' must always be called from a synchronized context?

Because wait() and notify() are based on the monitor of the object. If a thread calls object.wait() without owning the monitor, it throws an IllegalMonitorStateException. Furthermore, the wait() method is designed to atomically release the lock and put the thread to sleep, which is only possible if the thread holds the lock to begin with.

What is a 'Race Condition' versus a 'Data Race' in Java?

A 'Data Race' occurs when two threads access the same memory location concurrently and at least one is a write, without a happens-before relationship. A 'Race Condition' is a higher-level flaw where the correctness of a program depends on the relative timing of threads (e.g., Check-Then-Act). You can have a race condition even without a data race if your locking is too granular.

What is the difference between Runnable and Callable?

Both represent tasks intended for concurrent execution. However, Runnable.run() returns void and cannot throw checked exceptions. Callable.call() returns a Generic type <V> and can throw checked exceptions, making it the preferred choice for tasks that produce a result (retrieved via a Future).

Can volatile cause instruction reordering?

Volatile establishes happens-before relationships, which prevent certain reorderings. Specifically, reads and writes to volatile variables cannot be reordered with each other or with surrounding memory operations. But volatile does not prevent all reorderings — only those that would break the visibility guarantees. The JVM inserts memory barriers (e.g., LoadLoad, StoreStore) to enforce the ordering constraints.

What is the difference between Thread.interrupt() and a volatile flag for cancellation?

Thread.interrupt() sets the interrupt flag of the thread. Code that checks the flag via isInterrupted() or by catching InterruptedException can respond immediately. A volatile flag requires the thread to explicitly check it; if the thread is blocked in an I/O operation or in wait/sleep/join, it won't see the flag until it wakes up. Interrupt can wake up a sleeping thread, while a volatile flag cannot. So for responsive cancellation, interrupt is preferred.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousJava Collections Interview QuestionsNext →Java 8 Interview Questions
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged