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, then resourceB.

  • Thread-2 tries to acquire resourceB first, then resourceA.

  • 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?

  1. Thread-1 acquires resourceA.

  2. Thread-2 acquires resourceB.

  3. Each tries to acquire the other resource but fails.

  4. Both release their locks and retry.

  5. 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:

  1. Opposite lock acquisition order
    Each thread acquires resources in reverse order, guaranteeing contention.

  2. Immediate retries
    After failure, threads instantly retry, often clashing again.

  3. 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:

  1. Always acquire resourceA first.

  2. 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

0
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

Luis Gustavo Souza
Luis Gustavo Souza