skip to content

Thread Safety Concepts & Hazards

The strategies that make code thread-safe — immutability, confinement, safe publication — and the hazards that show up when they are missing: races, deadlock, livelock and starvation. Almost every senior Java interview includes at least one question from this area.

part ofJavaoverview, primer and where to startread it →
on this pageshow

explore

questions

25

What is a deadlock in a multithreaded Java program, and can you give a concrete example of how it happens?

level: juniorimportance: must knowfreq 78%

answer

  1. Cyclic wait on locks held by each other
  2. Two threads, two locks, opposite acquisition order
  3. Program hangs, doesn't crash (liveness failure)
  4. Threads stuck in BLOCKED state
  5. Timing-dependent, hides in tests

basics

~20 s

A deadlock is when two or more threads each hold a lock the other needs, so all of them wait forever and none can make progress. Example: thread A holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1.

solid answer

~40 s

A deadlock is a state where two or more threads are each blocked waiting for a resource (typically a lock/monitor) that another blocked thread is holding, forming a cycle so none can ever proceed. The classic example: thread A does synchronized(lock1){ synchronized(lock2){...} } while thread B does synchronized(lock2){ synchronized(lock1){...} }. If A grabs lock1 and B grabs lock2 at the same moment, A waits for lock2 (held by B) and B waits for lock1 (held by A) forever. Deadlock is a liveness failure: the program does not crash, it just hangs with threads stuck in BLOCKED state. It's distinct from a livelock (threads active but making no progress) and starvation (a thread perpetually denied a resource). Reproduction is timing-dependent, so it can hide in testing and surface in production.

code

java · 14 lines
java
class Account { final Object lock = new Object(); long balance; }

void transfer(Account from, Account to, long amt) {
    synchronized (from.lock) {          // step 1: lock 'from'
        synchronized (to.lock) {        // step 2: lock 'to'
            from.balance -= amt;
            to.balance   += amt;
        }
    }
}

// Thread A: transfer(a, b, 10)  -> locks a, waits for b
// Thread B: transfer(b, a, 10)  -> locks b, waits for a
// If A and B interleave between step 1 and step 2, they deadlock.

go deeper

for a junior

Can define deadlock as threads waiting on each other forever and sketch the two-thread, two-lock example.

for a middle

Distinguishes deadlock from livelock/starvation, knows it's a liveness (not safety) failure and is timing-dependent.

for a senior

Frames it in terms of a cyclic wait-for graph and connects the example to lock-ordering as the fix.

for a principal

Discusses why deadlocks evade testing, the cost in distributed/DB contexts, and architectural choices that make deadlock structurally impossible.

## What a lock is In Java, when multiple threads share mutable data, you protect it with a **lock** (also called a **monitor**) so only one thread touches the data at a time. The `synchronized` keyword acquires the lock on entry and releases it on exit; `java.util.concurrent.locks.Lock` (e.g. `ReentrantLock`) does the same explicitly with `lock()`/`unlock()`. While one thread holds a lock, any other thread that tries to acquire the *same* lock is parked in the **BLOCKED** state until the holder releases it. ## What a deadlock is A **deadlock** happens when a set of threads are blocked **in a cycle**: each thread in the set holds a lock that the next thread in the cycle is waiting for. Because every thread is waiting and none is running, no lock is ever released, so the wait is permanent. The program does not throw an exception or crash — it simply **hangs**. This is a **liveness** failure (the program fails to make progress) as opposed to a **safety** failure (the program produces wrong results). ## The canonical two-lock example Imagine a bank transfer that locks the *from* account then the *to* account: ``` Thread A: transfer(acct1 -> acct2) locks acct1, then tries to lock acct2 Thread B: transfer(acct2 -> acct1) locks acct2, then tries to lock acct1 ``` Interleaving that deadlocks: 1. A acquires acct1's lock. 2. B acquires acct2's lock. 3. A tries to acquire acct2 -> blocked (B holds it). 4. B tries to acquire acct1 -> blocked (A holds it). Now A waits for B and B waits for A: a cycle. Neither will ever release, so both hang forever. ## Why it's sneaky The deadlock only occurs if the two threads interleave at *just* the wrong moment (A grabbing lock1 while B grabs lock2). Most of the time one thread finishes before the other starts, so tests pass and the bug appears intermittently — often only under production load. That timing dependence is what makes deadlocks notoriously hard to catch. ## Related but different failures - **Livelock:** threads are not blocked — they keep responding to each other (e.g. both back off and retry in lockstep) but make no progress. - **Starvation:** a thread is perpetually denied a resource it needs (e.g. low-priority threads never scheduled), while others proceed. Deadlock specifically means a *cyclic, permanent* wait. ## How you'd derive an answer Think: shared locks + a cycle of who-waits-for-whom + permanence. If you can describe two threads acquiring two locks in opposite orders, you've described the simplest deadlock.

  • Can a single thread deadlock itself on one ReentrantLock?
    No. ReentrantLock (and synchronized) are reentrant: the same thread can re-acquire a lock it already holds without blocking. A self-deadlock needs a non-reentrant lock, or two locks acquired in conflicting order across threads.
  • How is deadlock different from a livelock?
    In a deadlock the threads are blocked (BLOCKED state) and do nothing. In a livelock they're active and keep changing state in response to each other but still make no forward progress, e.g. two threads that both detect contention and politely back off forever.

saying these in an interview costs you the question

  • Confusing deadlock (cyclic permanent block) with livelock (active but no progress) or starvation
  • Saying a deadlock throws an exception or crashes the JVM — it hangs silently
  • Thinking a single lock can deadlock by itself (reentrant locks allow the same thread to re-acquire)
  • Claiming deadlock always reproduces — it's timing-dependent

context

open as a page

What is thread confinement, and how does stack confinement of local variables make code thread-safe for free?

level: juniorimportance: must knowfreq 65%

basics

~20 s

Thread confinement means data is only ever touched by one thread, so no other thread can interfere and no locking is needed. Stack confinement is the special case where local variables live on one thread's stack and are never shared, making them automatically safe.

open as a page

Why are immutable objects inherently thread-safe, and what makes a Java class truly immutable?

level: juniorimportance: must knowfreq 78%

basics

~20 s

An immutable object never changes after it is built, so no thread can modify it while another reads it. With no shared mutable state to corrupt, no locking is needed. Make fields final, set them once in the constructor, and add no setters.

open as a page

What is a 'liveness failure' in concurrent programming, and how do deadlock, livelock, and starvation differ?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A liveness failure means threads never make progress. Deadlock: threads are blocked forever waiting on each other. Livelock: threads keep changing state reacting to each other but get nothing done. Starvation: one thread is perpetually denied a resource it needs.

open as a page

What is a race condition in Java, and why does it lead to nondeterministic, data-dependent bugs?

level: juniorimportance: must knowfreq 85%

basics

~20 s

A race condition is when two or more threads touch the same shared data at the same time without coordination, and the result depends on which thread happens to run first. Because timing varies, you get different, sometimes wrong, results each run.

open as a page

What is a ThreadLocal in Java, and what problem does it solve?

level: juniorimportance: must knowfreq 70%

basics

~10 s

A ThreadLocal gives each thread its own private copy of a variable. Threads never see each other's value, so you avoid sharing and locking. You read with get() and write with set().

open as a page

What are the four Coffman conditions for deadlock, and why must all four hold simultaneously?

level: middleimportance: must knowfreq 62%

basics

~10 s

The four Coffman conditions are: mutual exclusion, hold-and-wait, no preemption, and circular wait. A deadlock can only happen when all four are true at once, so breaking any one of them prevents deadlock.

open as a page

How do you diagnose a hung Java application you suspect is deadlocked, using a thread dump?

level: middleimportance: must knowfreq 58%

basics

~20 s

Take a thread dump (jstack <pid>, jcmd, or kill -3) and look for threads in BLOCKED state. The JVM usually prints a 'Found one Java-level deadlock' section naming the threads and the locks they each hold and wait for.

open as a page

How can a well-intentioned fix for deadlock (releasing locks and retrying) actually cause livelock, and how do you prevent it?

level: middleimportance: must knowfreq 60%

basics

~20 s

If a thread that can't get all its locks releases them and retries, and every competing thread does the same in lockstep, they keep releasing and retrying together and none ever finishes. Fix it by adding randomness or backing off for different amounts of time.

open as a page

Explain the check-then-act race condition with a concrete Java example, and show why it is unsafe even with thread-safe collections.

level: middleimportance: must knowfreq 72%

basics

~20 s

Check-then-act is when you test a condition and then act on it as separate steps. Another thread can change things between the check and the act, so your action is based on stale information. Example: "if absent, put" can let two threads both put.

open as a page

How does a consistent global lock-ordering prevent deadlock, and how do you order locks that have no natural ordering (e.g. two account objects)?

level: seniorimportance: must knowfreq 55%

basics

~20 s

If every thread always acquires locks in the same order, no cycle of waiting can form, so no deadlock. When objects have no natural order, derive one from a stable key like System.identityHashCode, and use a tie-breaker lock for rare hash collisions.

open as a page

What is safe publication, and why can sharing an object across threads be broken even when the object itself is correct?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Safe publication means handing an object to another thread in a way that guarantees that thread sees it fully built, not half-initialized. Without it, the receiving thread might see a null or stale fields due to reordering and CPU caches, even if the object is correct.

open as a page

Why can ThreadLocal cause a memory leak with thread pools, and how do you prevent it?

level: seniorimportance: must knowfreq 68%

basics

~10 s

Pool threads are reused and never die, so a value you set() stays attached to the thread forever and is seen by later tasks. Always call remove() in a finally block when you're done.

open as a page

Walk through the core ThreadLocal API: get, set, remove, and withInitial/initialValue. What does each do and what are the gotchas?

level: juniorimportance: should knowfreq 48%

basics

~10 s

set(v) stores a value for the current thread, get() reads it, remove() deletes it. withInitial(supplier) or overriding initialValue() gives a default the first time get() runs before any set().

open as a page

How do immutable, effectively immutable, unmodifiable, and a final field differ, and when do you reach for each?

level: middleimportance: should knowfreq 52%

basics

~20 s

Immutable means the object can never change. Effectively immutable means it could change but you never change it after sharing. Unmodifiable usually means a read-only wrapper over a still-mutable backing object. A final field just stops the reference from being reassigned.

open as a page

When should you reach for ThreadLocal instead of synchronization, and what are the trade-offs?

level: middleimportance: should knowfreq 50%

basics

~20 s

Use ThreadLocal when each thread can have its own copy and doesn't need to share — like reusing a SimpleDateFormat per thread. Use synchronization when threads must actually share and agree on the same data.

open as a page

How does ReentrantLock.tryLock with a timeout help avoid or recover from deadlock, and what are its trade-offs versus a fixed lock-ordering?

level: seniorimportance: should knowfreq 48%

basics

~20 s

tryLock(timeout) tries to grab a lock but gives up after a timeout instead of waiting forever. If a thread can't get all the locks it needs, it releases the ones it holds and retries later, so a forming deadlock breaks itself instead of hanging.

open as a page

You're told a service is 'hung.' How would you tell whether it's suffering from deadlock, livelock, or starvation?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Check CPU usage and thread dumps. Near-zero CPU with threads BLOCKED in a cycle on each other's locks = deadlock. High CPU but no progress = livelock. The system works but one specific thread never advances = starvation.

open as a page

What causes thread starvation in Java, and how do fairness settings and thread priorities affect it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Starvation happens when a thread never gets a resource it needs. In Java it can come from a thread holding a lock too long, unfair locks that let other threads barge ahead, or low thread priority making the scheduler skip it. Using a fair lock can prevent lock starvation.

open as a page

What techniques make a read-modify-write operation (like an increment) atomic in Java, and what are the trade-offs between them?

level: seniorimportance: should knowfreq 64%

basics

~20 s

Make the read, change, and write happen as one indivisible step. You can use a synchronized block or a Lock, or an atomic class like AtomicInteger with incrementAndGet, or a concurrent collection's atomic methods like compute. Each guarantees no other thread interleaves in the middle.

open as a page

How does InheritableThreadLocal differ from ThreadLocal, and what are its limitations?

level: seniorimportance: should knowfreq 45%

basics

~10 s

A normal ThreadLocal value is not visible to threads you start. InheritableThreadLocal copies the parent's value into a child thread when that child is created, so the child starts with the same value.

open as a page

Beyond lock-ordering and timeouts, what architectural strategies eliminate deadlock risk by design, and what are their trade-offs?

level: principalimportance: should knowfreq 34%

basics

~20 s

Avoid holding multiple locks at once: shrink each critical section, use immutable data and message passing so threads don't share mutable state, or use lock-free/single-threaded designs. If no thread ever waits on a second lock, the circular-wait condition can't occur.

open as a page

Given a piece of shared state, how do you decide between immutability, thread confinement, and locking, and what are the trade-offs?

level: principalimportance: should knowfreq 40%

basics

~20 s

First try to avoid sharing: keep data on one thread (confinement) or make it unchangeable (immutability) so no locks are needed. Only when you must share mutable state do you add locks or concurrent data structures, accepting their cost and complexity.

open as a page

How would you detect, reproduce, and design out race conditions in a Java codebase before they reach production?

level: principalimportance: should knowfreq 48%

basics

~20 s

Reduce shared mutable state first (immutability and confinement), so most code can't race. For what's left, use thread-safe constructs, review for unsynchronized shared access, and use stress/concurrency tests and analysis tools (jcstress, static analyzers) to surface the rare interleavings.

open as a page

When designing a high-throughput concurrent system, how do you weigh fairness, backoff, and lock-free design to avoid liveness failures without crippling throughput?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Avoid sharing locks where you can: prefer lock-free or per-thread/per-shard data and work queues. Where you must lock, use a consistent lock order to prevent deadlock, keep critical sections tiny, and only turn on fairness or backoff where a starved or livelocked path actually hurts, since both cost throughput.

open as a page