Understanding Livelock in Java — and How to Fix It


Concurrency issues are among the trickiest challenges in software development. While many developers are familiar with deadlocks, a lesser-known but equally problematic situation is the livelock.
In this post, we’ll explore what a livelock is, walk through a Java example that demonstrates it, and then explain a clean solution to avoid it.
What is a Livelock?
A livelock happens when two or more threads are not blocked — they’re actively running — but still fail to make any progress.
This is different from a deadlock: in a deadlock, threads are stuck waiting forever. In a livelock, threads remain “busy,” repeatedly reacting to each other in a way that prevents useful work from happening.
Deadlock: threads stop.
Livelock: threads run, but go nowhere.
Imagine two people politely stepping aside in a hallway to let the other pass. Each time one moves left, the other also moves left. Then they both move right — again at the same time. They keep moving but never cross the hallway. That’s a livelock.
The LiveLock problem
The demo program involves two shared resources: resourceA
and resourceB
.
Thread-1 tries to acquire
resourceA
first, thenresourceB
.Thread-2 tries to acquire
resourceB
first, thenresourceA
.Both threads use
tryLock
with a timeout, so they don’t deadlock. If a thread fails to acquire the second lock, it releases the first one and retries.
What goes wrong?
Thread-1 acquires
resourceA
.Thread-2 acquires
resourceB
.Each tries to acquire the other resource but fails.
Both release their locks and retry.
The pattern repeats indefinitely.
The program prints logs like:
Thread-1: locked resourceA
Thread-2: locked resourceB
Thread-2: couldn't lock resourceA, unlocking resourceB...
Thread-1: couldn't lock resourceB, unlocking resourceA...
Thread-1: locked resourceA
Thread-2: locked resourceB
...
The threads are alive and looping, but the task is never completed — that’s livelock in action.
Why Livelock Happens
The root causes in this example are:
Opposite lock acquisition order
Each thread acquires resources in reverse order, guaranteeing contention.Immediate retries
After failure, threads instantly retry, often clashing again.Timeout-based
tryLock
While useful to prevent deadlock, it doesn’t prevent livelock. The threads keep yielding to each other without resolution.
The LiveLock Solution: Consistent Lock Ordering
The fix is surprisingly simple: enforce the same resource acquisition order for all threads.
Instead of one thread grabbing resourceA
first and the other grabbing resourceB
first, both should follow the same sequence:
Always acquire
resourceA
first.Then acquire
resourceB
.
This eliminates the cycle of contention.
Example Behavior After Fix
Thread-1 acquires
resourceA
.Thread-2 waits for
resourceA
.Thread-1 then acquires
resourceB
, performs its task, and releases both locks.Now Thread-2 can proceed in the same order.
The logs now show smooth progress:
Thread-1: locked resourceA
Thread-1: performing task with both resources...
Thread-1: released resourceA and resourceB
Thread-2: locked resourceA
Thread-2: locked resourceB
Thread-2: performing task with both resources...
Thread-2: released resourceA and resourceB
No more endless retries — the livelock is gone.
Conclusion
Concurrency problems like livelocks can be subtle, since the program appears “alive” yet achieves nothing. They’re harder to spot than deadlocks but can be just as harmful in production.
When designing multithreaded applications, always ensure a consistent lock acquisition order. This simple discipline prevents both deadlocks and livelocks, keeping your concurrent code safe, efficient, and reliable.
You can check this code on github
Subscribe to my newsletter
Read articles from Luis Gustavo Souza directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
