Livelock & starvation in Java
Threads busy but not progressing; unfair scheduling.
Busy, going nowhere
In a livelock, threads aren't blocked: they keep reacting to each other, so nobody makes progress. Example: two services detect a conflict, both roll back, and both retry with identical timing. They collide again, and again. CPU sits at 100%, yet no transaction ever completes.
What does the dump show?
Livelocked threads: what state do they usually show in a thread dump?
Think about it, then reveal the answer
Usually RUNNABLE: they're executing code, just not making progress. Deadlock looks like BLOCKED threads and an idle CPU; livelock looks like a busy CPU with zero completed work. That's why livelock is harder to spot than deadlock.
Breaking the symmetry
while (!tryCommit()) {
rollback();
// retry immediately
}Both sides retry at the same instant and collide forever. Retrying *faster* makes it worse.
while (!tryCommit()) {
rollback();
Thread.sleep(base + rnd.nextInt(base));
}A random delay breaks the symmetry: one side retries first and wins.
Starvation
Starvation: a thread is perpetually denied its turn because others keep winning. Picture 50 threads hammering a lock in a tight loop while one low-priority reporting thread almost never gets it. Intrinsic locks are unfair: a newcomer may barge in ahead of threads that have waited longer.
Fair locks
new ReentrantLock(true) creates a fair lock that favors the longest-waiting thread, granting access roughly in arrival order. The price is lower throughput, in exchange for predictability. Thread priorities are only hints to the OS, so they're not a reliable cure.
Lock lock = new ReentrantLock(true); // fair
lock.lock();
try {
report();
} finally {
lock.unlock();
}Know your four villains
Deadlock: blocked forever, waiting on each other. Livelock: busy retrying, never progressing. Starvation: one thread perpetually denied access. Race condition: the result depends on unlucky timing. Each has a different symptom, and a different fix.
Retries in the cloud
When a request fails, thousands of clients retrying on the same schedule can hammer a recovering service in synchronized waves. That's why AWS and most retry libraries recommend exponential backoff with jitter: randomness spreads retries out so they stop colliding.
Key takeaways
- Deadlock: stuck waiting. Livelock: busy, but no progress
- Fix livelocks with randomized (jittered) backoff
- Starvation: one thread is perpetually denied its turn
- Fair locks (new ReentrantLock(true)) reduce starvation
💡 Two polite people in a corridor keep stepping aside in the same direction — both moving, neither getting past.
Classic Ethernet solved collisions the same way: after two stations collide, each waits a random number of time slots before resending.
Practice questions
Two services detect a conflict, roll back, and immediately retry with identical timing. CPU sits at 100% but no transaction ever completes. Diagnosis?
- Livelock
- Deadlock
- Starvation
- Memory leak
Check your answer
Livelock. Both sides are actively running and reacting to each other, so it's a livelock, not a deadlock.
How do you fix that retry livelock?
- Add a randomized (jittered) backoff before retrying
- Retry faster so one side wins
- Remove the conflict check
- Raise both threads' priorities
Check your answer
Add a randomized (jittered) backoff before retrying. Random delays break the symmetry, so one side retries first and succeeds. That's why network protocols and retry libraries use jittered backoff.