Java Tutorial
🔍

Livelock

Livelock

Livelock is deadlock's more active cousin — instead of threads frozen forever waiting for each other, livelocked threads are busy, actively responding to one another, and still never make any real progress. The classic analogy is two people meeting in a narrow corridor, each stepping aside to let the other pass, only for both to step the same direction at the same moment, over and over.

What Is Livelock?

Livelock is what happens when threads treat "keep trying" as automatically safer than "wait your turn." Both threads stay fully active — genuinely executing instructions the entire time — yet neither one ever finishes what it started.

The single biggest practical difference from deadlock: a deadlocked thread is parked and consumes essentially no CPU, while a livelocked thread is genuinely running the whole time, which is often the first diagnostic clue distinguishing the two.

Why Thread Safety Matters Here

A livelock is easy to miss precisely because the threads involved look healthy from the outside — they are RUNNABLE, not BLOCKED, so none of the usual deadlock-detection tooling flags anything wrong. What actually shows up is unexplained high CPU usage paired with an application that simply is not getting any work done, a combination that sends teams looking at the wrong layer of the system first, since high CPU more often points to legitimate heavy computation than to a stalled retry loop.

How It Works

One sentence before the diagram: the following trace shows two threads retrying in perfect lockstep, each releasing what it holds the instant it cannot get everything it needs.

Thread A                              Thread B
acquire Lock 1                        acquire Lock 2
try Lock 2 -> fails (B holds it)      try Lock 1 -> fails (A holds it)
release Lock 1                        release Lock 2
   |                                     |
   +---------- both retry immediately ---+
   |                                     |
   (identical cycle repeats forever, in sync)

Retrying immediately the moment a resource can't be fully acquired seems polite — release what you have and try again rather than block — but if two threads do this in perfect, unbroken lockstep, neither ever holds everything it needs at once.

1// File: LivelockProneSpinner.java 2// Illustrative only - do not run this to completion. If both threads keep 3// retrying in lockstep with no variation, neither ever holds both locks 4// long enough to proceed - this can spin indefinitely. 5import java.util.concurrent.locks.ReentrantLock; 6 7public class LivelockProneSpinner { 8 9 static void tryBothLocks(ReentrantLock first, ReentrantLock second, String label) { 10 while (true) { 11 if (first.tryLock()) { 12 try { 13 if (second.tryLock()) { 14 try { 15 System.out.println(label + " proceeded"); 16 return; 17 } finally { 18 second.unlock(); 19 } 20 } 21 } finally { 22 first.unlock(); 23 } 24 } 25 // politely back off and retry immediately - if both threads 26 // do this in sync, neither ever gets both locks at once 27 } 28 } 29 30 public static void main(String[] args) { 31 ReentrantLock aisleStart = new ReentrantLock(); 32 ReentrantLock aisleEnd = new ReentrantLock(); 33 34 Thread robotA = new Thread(() -> tryBothLocks(aisleStart, aisleEnd, "Robot A")); 35 Thread robotB = new Thread(() -> tryBothLocks(aisleEnd, aisleStart, "Robot B")); 36 37 robotA.start(); 38 robotB.start(); 39 } 40}

This kind of retry loop shows up most often in three places: retry logic between two cooperating threads or processes, each backing off symmetrically the moment it detects contention, exactly as LivelockProneSpinner illustrates; message-passing protocols where both sides politely defer to the other with no tie-breaking rule, a pattern that shows up in distributed systems just as easily as within a single JVM; and resource negotiation without a deterministic tie-breaker, where two competing operations retry using exactly the same logic and exactly the same conditions, so symmetry itself becomes the bug.

Code Examples

Requesting the same two locks in the same fixed order, regardless of which "direction" each caller is going, removes the symmetric back-and-forth entirely.

1// File: LivelockFreeSpinner.java 2import java.util.concurrent.locks.ReentrantLock; 3import java.util.concurrent.atomic.AtomicInteger; 4 5public class LivelockFreeSpinner { 6 7 static void acquireBothInOrder(ReentrantLock first, ReentrantLock second, AtomicInteger completed) { 8 first.lock(); 9 try { 10 second.lock(); 11 try { 12 completed.incrementAndGet(); 13 } finally { 14 second.unlock(); 15 } 16 } finally { 17 first.unlock(); 18 } 19 } 20 21 public static void main(String[] args) throws InterruptedException { 22 ReentrantLock aisleStart = new ReentrantLock(); 23 ReentrantLock aisleEnd = new ReentrantLock(); 24 AtomicInteger completed = new AtomicInteger(); 25 26 // Both robots always request aisleStart first, aisleEnd second - 27 // the same fixed order removes the symmetric back-and-forth entirely 28 Thread robotA = new Thread(() -> acquireBothInOrder(aisleStart, aisleEnd, completed)); 29 Thread robotB = new Thread(() -> acquireBothInOrder(aisleStart, aisleEnd, completed)); 30 31 robotA.start(); 32 robotB.start(); 33 robotA.join(); 34 robotB.join(); 35 36 System.out.println("Robots that passed: " + completed.get()); 37 } 38}
Output:
Robots that passed: 2

Using plain, blocking lock() in a fixed order instead of tryLock()-and-retry converts the situation entirely — whichever thread arrives first simply makes the other wait normally, with no possibility of the symmetric stepping-aside pattern at all.

Real-World Example

Two warehouse robots share a single-lane aisle, and both need to hold the aisle's entry and exit locks at once to pass through — modeled with a shared navigator class that guarantees every caller acquires those two locks in the same order.

1// File: WarehouseRobot.java 2 3public class WarehouseRobot { 4 private final String name; 5 6 public WarehouseRobot(String name) { 7 this.name = name; 8 } 9 10 public String getName() { 11 return name; 12 } 13}
1// File: AisleNavigator.java 2import java.util.concurrent.locks.ReentrantLock; 3 4public class AisleNavigator { 5 private final ReentrantLock aisleStart = new ReentrantLock(); 6 private final ReentrantLock aisleEnd = new ReentrantLock(); 7 8 public String passThrough(WarehouseRobot robot) { 9 aisleStart.lock(); 10 try { 11 aisleEnd.lock(); 12 try { 13 return robot.getName() + " passed through the aisle"; 14 } finally { 15 aisleEnd.unlock(); 16 } 17 } finally { 18 aisleStart.unlock(); 19 } 20 } 21}
1// File: AisleNavigatorDemo.java 2import java.util.concurrent.atomic.AtomicInteger; 3 4public class AisleNavigatorDemo { 5 public static void main(String[] args) throws InterruptedException { 6 AisleNavigator navigator = new AisleNavigator(); 7 WarehouseRobot robotA = new WarehouseRobot("Robot-A"); 8 WarehouseRobot robotB = new WarehouseRobot("Robot-B"); 9 AtomicInteger passed = new AtomicInteger(); 10 11 Thread t1 = new Thread(() -> { 12 navigator.passThrough(robotA); 13 passed.incrementAndGet(); 14 }); 15 Thread t2 = new Thread(() -> { 16 navigator.passThrough(robotB); 17 passed.incrementAndGet(); 18 }); 19 20 t1.start(); 21 t2.start(); 22 t1.join(); 23 t2.join(); 24 25 System.out.println("Robots that passed: " + passed.get()); 26 } 27}
Output:
Robots that passed: 2

A mistake that appears often in fresher pull requests is having two competing operations each retry immediately and unconditionally the moment they can't get everything they need, assuming that "trying again" is inherently safer than blocking and waiting — when both sides retry in exactly the same way, that assumption is precisely what causes neither to ever make progress. Having every caller acquire the aisle's two locks through one shared method, exactly as passThrough() does here, removes the possibility of two callers ever requesting them in a conflicting order in the first place.

Best Practices

Prefer blocking lock() with consistent ordering over a tryLock()-and-retry-forever pattern whenever two or more resources need to be acquired together — this is the same fix this section's Deadlock article uses, since the two bugs share a root cause: poorly coordinated access to shared resources.

If retry-based coordination genuinely is needed, add randomized backoff or a small amount of jitter between attempts, so two competing retries are extremely unlikely to stay in lockstep indefinitely.

Give competing operations a deterministic tie-breaker — a stable priority or an ID comparison — instead of purely symmetric retry behavior, so one side reliably wins and the other reliably waits.

Cap any retry loop with a maximum attempt count that escalates to logging or failing loudly, rather than retrying forever with no visibility that anything is wrong.

Common Mistakes

Assuming tryLock()-based "polite" retry logic is inherently safer than plain, blocking lock() is the core misconception behind most livelock bugs — a tryLock()-and-immediately-retry loop with no backoff or tie-breaker is exactly the pattern that causes livelock, while blocking lock() combined with consistent ordering has no such risk at all.

Adding a retry loop with no upper bound or escalation path compounds the danger — even a version with some randomized variation can still spin persistently on an unlucky sequence of timing, and more importantly gives no signal that anything has gone wrong. A bounded retry count that logs or fails after a fixed number of attempts turns a silent, CPU-burning stall into a visible, diagnosable failure.

Interview Questions

Q1. What is livelock, and how does it differ from deadlock?

Livelock is a state where threads remain active and keep responding to each other, but no thread ever makes real progress. Deadlock is a state where threads are completely frozen, each waiting on a resource the other holds. The key practical difference is CPU usage — a deadlocked thread is parked and idle, while a livelocked thread is genuinely busy the whole time. Interviewers listen specifically for that CPU distinction, since it is how the two are actually told apart in a real incident.

Q2. What is the classic real-world analogy used to explain livelock?

Two people meeting in a narrow corridor, each politely stepping aside to let the other pass, only for both to step the same direction at the same time — repeating indefinitely, with both people fully active but neither ever getting through. This question is mostly a warm-up; interviewers use it to check you actually understand the concept before asking for the Java-level mechanism.

Q3. Can tryLock() based retry logic cause livelock?

Yes, if two threads retry in a symmetric, unconditional way with no variation or tie-breaker, exactly as demonstrated in this article's LivelockProneSpinner example — the retry pattern itself is what creates the risk, not tryLock() alone. The nuance interviewers want is that you don't blame tryLock() itself, but the unconditional retry built around it.

Q4. How does consistent lock ordering prevent the two-lock livelock scenario?

If every thread always acquires the same set of locks in the same fixed order using blocking lock() calls, whichever thread arrives first simply makes the other wait normally — there is no possibility of the symmetric release-and-retry pattern that causes livelock in the first place. This is often asked right after the deadlock lock-ordering question specifically to see whether you notice the fix is the same for both bugs.

Q5. Is livelock a safety problem or a liveness problem?

A liveness problem. No thread produces an incorrect result the way a race condition would — the problem is that no thread produces any result at all, since the system never actually makes forward progress. Interviewers use this to check whether you can classify concurrency bugs by category, not just describe symptoms.

Q6. What is randomized exponential backoff, and how does it help avoid livelock?

It is a retry strategy where each failed attempt waits a randomly increasing amount of time before retrying again. Introducing randomness breaks the lockstep symmetry that causes livelock, making it extremely unlikely — though not mathematically guaranteed — that two competing retries stay perfectly synchronized indefinitely. Product-company interviewers often push on the "not guaranteed" part specifically, since it separates candidates who understand probability from those reciting a term.

Q7. Does a deadlocked thread consume CPU the way a livelocked thread does?

No. A deadlocked thread is parked by the JVM, waiting on a lock it will never receive, and consumes essentially no CPU. A livelocked thread is actively executing a retry loop the entire time, so it does consume real CPU — a livelock often shows up as unexpectedly high CPU usage with no forward progress, while a deadlock shows up as threads sitting idle. This is the single most commonly repeated interview question on this topic across both service and product companies.

FAQs

Is livelock more or less common than deadlock in real systems?

Deadlock is more commonly discussed and detected, partly because tools like jstack explicitly flag it. Livelock is arguably harder to notice, since the affected threads look busy rather than obviously stuck, and often needs a closer look at what a thread is actually doing rather than just whether it is blocked.

Can more than two threads be involved in a livelock?

Yes. Any number of threads that keep actively responding to each other's state changes without ever settling into a state where all of them can proceed simultaneously can form a livelock, not just the classic two-thread case.

Does livelock always resolve itself eventually?

No, not guaranteed — a purely symmetric retry pattern with no variation can in principle continue indefinitely. Introducing randomized backoff or a deterministic tie-breaker is what actually gives a livelock a reliable way to resolve.

Is livelock detectable with the same tools used for deadlock, like jstack?

Not in the same direct way — jstack explicitly flags deadlocked threads, but livelocked threads show up as ordinary RUNNABLE threads, since they are genuinely executing. Noticing a livelock usually means observing that several threads are consuming CPU without the application making any actual progress.

Can livelock occur in distributed systems, not just within a single JVM?

Yes, and this is actually one of the more common places it shows up — two services or nodes that both back off and retry a request in response to the other's retry, using identical logic, can livelock across a network exactly as two threads can within one process.

Is randomized backoff a guaranteed fix for livelock?

Not mathematically guaranteed, but practically very effective — introducing enough randomness makes two competing retries staying in lockstep for long increasingly unlikely with every attempt, even though a specific unlucky sequence remains theoretically possible.

Does virtual threads change how livelock works?

Not fundamentally — a livelocked virtual thread is still busy-retrying and still consumes CPU exactly as a platform thread would. If the retry loop involves a synchronized block, that block pins the virtual thread to its carrier for as long as it is held on each attempt, which can make a livelock involving synchronized specifically more expensive under virtual threads than an equivalent one using Lock.

Summary

Livelock happens when threads stay active and keep responding to each other, yet none of them ever actually makes progress — the corridor-stepping-aside pattern playing out in code, usually caused by symmetric, unconditional retry logic with no tie-breaker. The most reliable fix is the same one this section's Deadlock article uses: consistent lock ordering with plain, blocking lock() calls, which removes the retry-and-back-off pattern that causes livelock in the first place.

The habit worth carrying forward from this article's warehouse robot example is treating any tryLock()-and-immediately-retry pattern with suspicion, and reaching for either consistent ordering or a deterministic tie-breaker before ever reaching for randomized backoff as the fix.

What to Read Next