🧵 Concurrency Fundamentals · Advanced

Livelock & starvation in Java

Threads busy but not progressing; unfair scheduling.

🧩 The mysteryTwo people meet in a corridor. Both step left. Both step right. Both step left again... Everyone is moving, nobody gets past. That's a livelock.

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.

🤔 Think first

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

✗ Retry in lockstep
while (!tryCommit()) {
    rollback();
    // retry immediately
}

Both sides retry at the same instant and collide forever. Retrying *faster* makes it worse.

✓ Jittered backoff
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.

💼 In the real world

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

  1. Deadlock: stuck waiting. Livelock: busy, but no progress
  2. Fix livelocks with randomized (jittered) backoff
  3. Starvation: one thread is perpetually denied its turn
  4. 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.

🤯 Did you know?

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?

  1. Livelock
  2. Deadlock
  3. Starvation
  4. 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?

  1. Add a randomized (jittered) backoff before retrying
  2. Retry faster so one side wins
  3. Remove the conflict check
  4. 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.

Next: how does a thread sleep until there's work, without burning CPU? wait() and notify(), and the one-word mistake that breaks them.