Concurrency
Threads, synchronization, deadlock, volatile, and the wait/notify coordination primitives.
13 questions
JuniorTheoryVery commonWhat is a race condition in Java, and how do you fix it?
What is a race condition in Java, and how do you fix it?
A race condition is when a program's correctness depends on the unpredictable timing of two or more threads accessing shared mutable state at once. The classic case is two threads reading, incrementing, and writing the same counter, where interleaved steps lose an update. You fix it by serializing access with synchronized, a lock, or atomic operations.
Common mistakes
- ✗Thinking a race condition is a performance issue rather than a correctness one
- ✗Believing single-core machines are immune to race conditions
- ✗Forgetting that read-modify-write on shared state is not atomic without synchronization
Follow-up questions
- →Why is
counter++on a shared field not atomic, and what does that allow to go wrong? - →How do atomic classes like
AtomicIntegerremove a race without asynchronizedblock?
MiddleTheoryVery commonWhat exactly does the synchronized keyword do in Java?
What exactly does the synchronized keyword do in Java?
A synchronized method or block acquires the monitor lock of an object before entering, so only one thread at a time can execute that critical section guarded by the same lock. This serializes access to shared mutable state, preventing race conditions where interleaved reads and writes corrupt data. Releasing the lock on exit also flushes changes so the next thread sees them.
Common mistakes
- ✗Thinking
synchronizedparallelizes the body instead of serializing access to it - ✗Assuming it locks the whole JVM rather than one specific object's monitor
- ✗Synchronizing on different objects and expecting them to exclude each other
Follow-up questions
- →On which object does a
synchronizedinstance method versus a static method acquire its lock? - →How does releasing a monitor establish a happens-before relationship for visibility?
SeniorTheoryVery commonWhat is a deadlock in Java, and how do you prevent it?
What is a deadlock in Java, and how do you prevent it?
Deadlock is when two or more threads each hold a lock the other needs and wait forever, so none can proceed. The classic case is thread A holding lock 1 and wanting lock 2 while thread B holds lock 2 and wants lock 1. You prevent it by imposing a consistent global lock-ordering so all threads acquire locks in the same sequence, using tryLock with a timeout to back off, or by holding fewer locks at once.
Common mistakes
- ✗Treating deadlock as a slowness or priority issue rather than a circular lock wait
- ✗Thinking randomizing lock order prevents deadlock when consistent ordering is the cure
- ✗Ignoring that holding multiple locks at once is what enables the circular dependency
Follow-up questions
- →How does
tryLockwith a timeout break a potential deadlock that plain locking cannot? - →What are the four Coffman conditions that must all hold for a deadlock to occur?
JuniorTheoryCommonWhat are the ways to create a thread in Java, and which is preferred?
What are the ways to create a thread in Java, and which is preferred?
Two basic ways: extend Thread and override run(), or implement Runnable and pass it to a Thread. Implementing Runnable is preferred because it leaves the class's single inheritance slot free and separates the task from the worker. For tasks that return a result or throw a checked exception, implement Callable and submit it to an ExecutorService, which hands back a Future.
Common mistakes
- ✗Calling
run()directly instead ofstart(), which runs the code on the current thread - ✗Extending
Threadby default and wasting the single inheritance slot - ✗Thinking
Runnablecan return a value, when onlyCallableproduces aFutureresult
Follow-up questions
- →What happens if you call
run()directly instead ofstart()on aThread? - →How does a
Callablereturning aFuturediffer from aRunnablesubmitted to an executor?
MiddleDebuggingCommonFix the loop that starts threads and exits
Fix the loop that starts threads and exits
Two bugs. First, the lambda captures i, but a loop variable is not effectively final, so it does not compile — copy it into a final local (final int n = i;) and print n. Second, System.exit(0) terminates the JVM immediately, killing the sleeping threads before they print — remove it (or join each thread first) so the program waits for their output.
Common mistakes
- ✗Thinking a
forloop variable is effectively final and can be captured directly - ✗Overlooking that
System.exit(0)kills the JVM and the still-running threads - ✗Confusing the fix with adding
volatile, which addresses visibility, not capture or exit
Follow-up questions
- →Why is a
forloop variable not effectively final while a foreach variable can be? - →How would
joinon each thread change the program's behavior versus removingSystem.exit?
MiddleTheoryCommonWhat does the volatile keyword guarantee, and what does it not?
What does the volatile keyword guarantee, and what does it not?
volatile guarantees visibility: every read and write of the field goes straight to main memory rather than a thread-local cache, so other threads always observe the latest written value and the compiler cannot reorder around it. It does NOT provide atomicity for compound operations like i++, which is a read-modify-write that two threads can still interleave and lose updates on.
Common mistakes
- ✗Using
volatilefor a counter and expectingi++to be atomic across threads - ✗Confusing the visibility guarantee with mutual exclusion that
synchronizedprovides - ✗Thinking
volatilecaches the value locally rather than forcing main-memory access
Follow-up questions
- →When is a single
volatileboolean flag a correct and sufficient synchronization tool? - →Why does
AtomicIntegersucceed at an atomic increment where avolatile intfails?
MiddleTheoryCommonWhat is the difference between wait() and sleep() in Java?
What is the difference between wait() and sleep() in Java?
wait() is an Object method that RELEASES the monitor lock and parks the thread until another thread calls notify()/notifyAll() on the same object; it must be invoked while already holding that lock, normally inside a synchronized block. sleep() is a static Thread method that just pauses the current thread for a fixed duration and KEEPS every lock it currently holds.
Common mistakes
- ✗Calling
wait()outside asynchronizedblock, causing anIllegalMonitorStateException - ✗Believing
sleep()releases the held monitor locks the waywait()actually does - ✗Forgetting
wait()needs an externalnotify()to resume, not just a timer
Follow-up questions
- →Why must
wait()be called inside asynchronizedblock holding the object's monitor? - →Why should
wait()be invoked in a loop that re-checks its condition rather than anif?
SeniorTheoryCommonWhat is the difference between a race condition, a deadlock, and a livelock?
What is the difference between a race condition, a deadlock, and a livelock?
A race condition is a timing bug where threads interleave access to shared state and corrupt it; you fix it by serializing access with locks or atomics. A deadlock is threads each holding a lock the other needs and waiting forever; a consistent lock-ordering or tryLock timeouts prevent it. A livelock is threads that keep reacting to each other and changing state but make no progress; randomized back-off breaks it.
Common mistakes
- ✗Conflating the three — treating a deadlock and a livelock as the same stuck state
- ✗Calling a livelock a deadlock even though livelocked threads keep running and changing state
- ✗Thinking one mechanism like
synchronizedalone prevents all three hazards
Follow-up questions
- →How does a livelock differ from thread starvation, where one thread never gets scheduled?
- →Why does randomized back-off break a livelock when deterministic retry does not?
JuniorTheoryOccasionalWhat is the difference between a process and a thread in Java?
What is the difference between a process and a thread in Java?
A process is an independent running program with its own isolated memory space. A thread is a lightweight unit of execution inside a process; all threads of one process share that process's heap and loaded objects but each gets its own stack. Threads are cheaper to create than processes and can share data directly, which is fast but requires synchronization.
Common mistakes
- ✗Claiming threads have isolated memory like processes and cannot share objects directly
- ✗Believing threads and processes cost the same to create and switch between
- ✗Forgetting that shared heap access between threads needs synchronization to stay correct
Follow-up questions
- →Why does sharing data between threads need synchronization but sharing across processes rarely does?
- →What part of a thread is private to it, and what is shared with sibling threads?
MiddleTheoryOccasionalWhat does the Java Memory Model guarantee about visibility and ordering between threads?
What does the Java Memory Model guarantee about visibility and ordering between threads?
The memory model defines happens-before: if action A happens-before B, everything A wrote is visible to B and cannot be reordered past it. Releasing a monitor, writing a volatile field and starting a thread each create such an edge. Without one, reads may be reordered and a thread can keep seeing a stale value forever.
Common mistakes
- ✗Reasoning as if every write were instantly global, instead of needing a happens-before edge
- ✗Treating
synchronizedas mutual exclusion only, forgetting it also publishes writes - ✗Believing a
Thread.sleepor a longer delay eventually makes a plain write visible
Follow-up questions
- →Which actions besides a
volatilewrite establish a happens-before edge? - →Why can a loop on a plain boolean flag spin forever after another thread sets it?
MiddleTheoryOccasionalWhat does ThreadLocal give each thread, and how does InheritableThreadLocal differ?
What does ThreadLocal give each thread, and how does InheritableThreadLocal differ?
ThreadLocal gives every thread its own independent copy of a value, held in a map that hangs off the Thread object, so no synchronization is needed to read or write it. InheritableThreadLocal additionally copies the parent's value into a child at the moment the child thread is created. Values must be removed explicitly: a pooled thread outlives the task and keeps the entry alive.
Common mistakes
- ✗Skipping
remove()in a pooled executor, so the next task inherits a stale value or the entry leaks - ✗Assuming every thread you start inherits the parent's
ThreadLocalvalue - ✗Treating
ThreadLocalas a synchronization primitive rather than as per-thread storage
Follow-up questions
- →Why does a
ThreadLocalin a thread pool leak whenremove()is never called? - →Why does
InheritableThreadLocalfail to propagate a value through anExecutorService?
SeniorTheoryOccasionalWhat is the difference between notify() and notifyAll() in Java?
What is the difference between notify() and notifyAll() in Java?
notify() wakes a single arbitrary thread waiting on the object's monitor; you cannot choose which one. notifyAll() wakes every waiting thread, and they then re-contend for the lock, each re-checking its condition before proceeding. Prefer notifyAll() when threads wait on different conditions, because notify() might wake the wrong thread and leave a thread that could have run waiting forever.
Common mistakes
- ✗Assuming
notify()wakes the longest-waiting thread when the choice is arbitrary - ✗Thinking woken threads run immediately rather than re-contending for the monitor
- ✗Using
notify()with mixed wait conditions and stranding a runnable thread forever
Follow-up questions
- →In what restricted situation is
notify()safe and more efficient thannotifyAll()? - →Why must a woken thread re-check its condition in a loop after returning from
wait()?
SeniorDebuggingOccasionalDiagnose why two worker threads stopped making progress in this thread dump
Diagnose why two worker threads stopped making progress in this thread dump
Both threads are BLOCKED on a monitor, and their monitor addresses cross: worker-1 holds 0x…41180 and waits for 0x…3f2c8, while worker-2 holds 0x…3f2c8 and waits for 0x…41180. That is a circular wait on two Account monitors — a deadlock, because transfer locks the accounts in the caller's argument order, so two opposite transfers grab them in opposite orders. Fix it with a consistent global lock order (e.g. by account id) or a timed tryLock with back-off.
Common mistakes
- ✗Reading
BLOCKEDas CPU starvation or scheduling delay instead of a monitor wait - ✗Ignoring the monitor addresses, which are what prove the wait is circular
- ✗Proposing a bigger pool or higher priority, which only adds more stuck threads
Follow-up questions
- →Which
jstackline would confirm the diagnosis automatically without reading the frames? - →How would switching
Accountmonitors to a timedtryLockchange this dump?