Concurrency & Threads

15 questions found

How does Java's virtual threads feature (Project Loom, finalized in Java 21) fundamentally change the thread-per-task model for high-concurrency applications, and how do they differ from traditional platform threads?

Advanced
Virtual threads are lightweight threads managed by the JVM itself (not directly mapped one-to-one to an OS thread), allowing an application to create potentially millions of virtual threads without the memory and context-switching overhead that would make an equivalent number of traditional platform threads impractical -- when a virtual thread performs a blocking operation (like I/O), the JVM automatically unmounts it from its underlying OS "carrier" thread and mounts a different virtual thread instead, letting a small pool of actual OS threads efficiently serve a vastly larger number of concurrent virtual threads, fundamentally enabling a simple "thread-per-request" programming model (easier to reason about than reactive/async code) to scale to workloads previously requiring complex asynchronous frameworks.
// Traditional: limited by OS thread overhead, typically capped around a few thousand concurrent threads
ExecutorService platformPool = Executors.newFixedThreadPool(200);

// Virtual threads: can create millions, each cheap, automatically unmounted during blocking I/O
try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        virtualExecutor.submit(() -> {
            // blocking I/O call here doesn't tie up a scarce OS thread
            makeBlockingHttpCall();
        });
    }
}
Real-world example A high-throughput API gateway migrates from a complex reactive (non-blocking, callback-heavy) architecture to a simple thread-per-request model using virtual threads, achieving comparable throughput and resource efficiency to the reactive approach while dramatically simplifying the codebase back to straightforward, easy-to-debug synchronous-looking code.

Common follow-ups: What specific blocking operations cause a virtual thread to be unmounted from its carrier thread, and are there operations that still "pin" a virtual thread (blocking the carrier)?;How does this change the calculus around whether to adopt a reactive programming model versus simple thread-per-request going forward?

Diagnostics & Performance;Design Patterns in Java

What is the difference between wait()/notify()/notifyAll() (Object's intrinsic monitor methods) and the newer java.util.concurrent tools, and why are the older methods considered error-prone?

Intermediate
wait()/notify()/notifyAll() are low-level primitives (must be called only while holding the associated object's monitor lock via synchronized, and wait() must always be called in a loop re-checking its condition due to the possibility of "spurious wakeups") for implementing custom condition-based thread coordination -- they're widely considered error-prone and difficult to use correctly (easy to miss the synchronized requirement, forget the while-loop condition check, or accidentally call notify() instead of notifyAll() and lose a waiting thread), which is why higher-level java.util.concurrent tools (CountDownLatch, Condition objects paired with ReentrantLock, BlockingQueue) are generally strongly preferred for virtually all modern coordination needs, reserving raw wait/notify largely for understanding how these higher-level tools are built internally.
// Error-prone raw wait/notify pattern (requires careful, easy-to-get-wrong discipline)
synchronized (lock) {
    while (!conditionMet) {  // MUST be a while loop, not if, due to spurious wakeups
        lock.wait();
    }
    // proceed once condition is met
}

// Preferred modern alternative: BlockingQueue handles all this coordination internally
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
Task task = queue.take();  // blocks safely until an item is available, no manual wait/notify needed
Real-world example A producer-consumer implementation initially hand-written with raw wait()/notify() (and a subtle bug from using notify() instead of notifyAll(), occasionally leaving a consumer thread stuck waiting forever) is refactored to use a simple BlockingQueue instead, eliminating an entire category of hard-to-diagnose coordination bugs by relying on a well-tested, higher-level abstraction.

Common follow-ups: What is a spurious wakeup, and why does this necessitate the while-loop condition re-check pattern?;How does BlockingQueue implement producer-consumer coordination internally, and does it still ultimately rely on similar low-level primitives?

Design Patterns in Java;Background Tasks & Hosted Services

What is the difference between calling Thread.start() and Thread.run() directly, and why is calling run() directly a common beginner mistake?

Beginner
start() causes the JVM to allocate a new OS-level thread and asynchronously invoke run() on that new thread of execution; calling run() directly instead simply invokes it as a completely ordinary synchronous method call on the CURRENT thread, with no new thread created at all -- this is a common beginner mistake because the code compiles and appears to run without any error, but the intended concurrent execution silently never actually happens, since run() executes synchronously and blocks the caller just like any normal method call would.
Thread t = new Thread(() -> System.out.println("Running on: " + Thread.currentThread().getName()));

t.start();  // correct: prints a NEW thread name, e.g. "Thread-0", runs concurrently
t.run();    // mistake: prints "main", runs synchronously on the CURRENT thread, no concurrency at all
Real-world example A beginner's attempt to run several tasks "concurrently" by calling run() directly on several Thread objects (instead of start()) is confused when execution is actually happening sequentially, one after another, since each run() call was really just a normal synchronous method invocation on the main thread the whole time.

Common follow-ups: Can you call start() more than once on the same Thread object?;What exception is thrown if you attempt to call start() twice on the same Thread instance?

Background Tasks & Hosted Services;Design Patterns in Java

What is a race condition, and how does it differ conceptually from a deadlock as two distinct categories of concurrency bugs?

Intermediate
A race condition occurs when the correctness of a program's outcome depends on the unpredictable relative timing/interleaving of multiple threads accessing shared mutable state without adequate synchronization, producing incorrect results (like lost updates) that may vary nondeterministically between runs; a deadlock, by contrast, is a liveness failure where threads become permanently stuck waiting for each other in a circular dependency, producing no forward progress at all rather than an incorrect result -- a race condition corrupts correctness silently, while a deadlock halts execution entirely and visibly (typically detectable via a thread dump showing threads stuck in BLOCKED state).
// Race condition example: unsynchronized shared counter increment
int[] counter = {0};
Runnable increment = () -> counter[0]++;  // read-modify-write, not atomic
// Running this concurrently from multiple threads produces an unpredictable, often INCORRECT final count

// Deadlock example: two threads waiting on each other's locks (see lock ordering question for full example)
// Produces NO progress at all, rather than a wrong-but-completed result
Real-world example A load test reveals a payment processing counter occasionally undercounts transactions under concurrent load (a race condition producing a silently wrong number), which is a fundamentally different class of bug from a separate incident where the same system occasionally hung entirely and stopped processing any transactions (a deadlock), requiring different diagnostic and fix approaches for each.

Common follow-ups: What tools or techniques would you use to detect each type of bug (race condition versus deadlock) in a running application?;Can a single bug exhibit characteristics of both a race condition and a deadlock simultaneously?

Diagnostics & Performance;Exceptions

How would you diagnose a suspected thread deadlock in a running production JVM using a thread dump, and what specific information in the dump reveals the deadlock?

Advanced
A thread dump (obtainable via jstack <pid>, or a JMX-based tool, or sending SIGQUIT to the JVM process on Unix) captures the current state and stack trace of every live thread at that moment -- the JVM's built-in deadlock detection (included automatically in a thread dump when applicable) explicitly identifies and reports any detected deadlock cycle, listing each involved thread, the lock it currently holds, and the lock it's waiting to acquire, letting you directly trace the circular dependency; threads stuck in a deadlock typically show a BLOCKED state along with a stack trace pointing at the exact synchronized block or lock.lock() call where they're stuck.
# Generate a thread dump for a running Java process
jstack <pid> > threaddump.txt

# The output includes an explicit section like:
# "Found one Java-level deadlock:"
# "Thread-0" waiting to lock monitor 0x... (object lockB), which is held by "Thread-1"
# "Thread-1" waiting to lock monitor 0x... (object lockA), which is held by "Thread-0"
Real-world example An on-call engineer investigating a production service that suddenly stopped responding to any requests captures a thread dump via jstack, immediately spotting the JVM's own explicit "Found one Java-level deadlock" report identifying the exact two threads and locks involved, dramatically speeding up root-cause diagnosis compared to guessing from application logs alone.

Common follow-ups: What's the difference between a thread in BLOCKED state versus WAITING versus TIMED_WAITING in a thread dump?;How would you capture a thread dump automatically as part of an incident response runbook without manual intervention?

Diagnostics & Performance;Incident Response

Showing 11–15 of 15