Both are liveness failures where threads make no progress, but for opposite reasons. In livelock threads are actively running and reacting to each other, yet never advance. In starvation a thread is ready to run but is perpetually denied the resource or CPU it needs.
Livelock
The threads are not blocked — they keep changing state, but in a way that cancels out. The textbook image is two people in a corridor who repeatedly step the same direction to let each other pass. In code it usually comes from polite conflict-resolution: a thread detects contention, backs off, retries, detects contention again, backs off again — in lockstep with another thread doing the same.
// Two threads each release their lock when they notice the other is stuck,
// then both retry at the same instant -> they swap, notice again, forever.
while (!acquiredBoth) {
lockA.lock();
if (!lockB.tryLock()) { // can't get B
lockA.unlock(); // be polite, release A and retry
continue; // ... but the other thread does exactly the same, in sync
}
acquiredBoth = true;
}
The cure is to desynchronize the retries — randomized/exponential backoff, or jittered timeouts — so the threads stop colliding at the same moment. CPU usage stays high during a livelock, which is one way to tell it apart from a deadlock (deadlocked threads are BLOCKED/WAITING and idle).
Starvation
A thread could make progress but never gets scheduled the resource. Causes:
- Unfair locks. A default
ReentrantLock(orsynchronized) gives no ordering guarantee; under heavy contention a thread can be barged past indefinitely. A fair lock (new ReentrantLock(true)) grants in FIFO order at a throughput cost. - Thread priorities. A flood of high-priority threads can starve lower-priority ones.
- Greedy holders. A thread that hogs a lock or a connection-pool slot for long periods starves the rest.