Starvation
Starvation is a liveness failure in which a thread is perpetually denied access to a resource it needs to make progress, not because of deadlock (no cycle of blocked threads exists) but because other threads are continuously given priority. The starving thread is alive, runnable, and not waiting on a condition — it simply never gets scheduled to execute, or never successfully acquires a lock it needs, because it is always losing the competition. Starvation can be caused by Java's unfair intrinsic lock scheduling (threads competing for synchronized are given no ordering guarantee), by thread priority misuse (high-priority threads monopolizing the CPU), by busy threads that hold locks for long durations, or by poorly designed lock protocols that systematically favor certain threads. This entry covers the mechanism of starvation in the JVM, unfair locking and why intrinsic locks can starve threads, thread priority and why it is an unreliable tool, fair locking with ReentrantLock(true), scheduling starvation from long-running tasks in thread pools, and how to detect and remediate starvation.
Starvation Mechanisms — Unfair Locks and Priority
// ── Intrinsic lock starvation: unfair contention ─────────────────────
public class UnfairCounter {
private int count = 0;
private final int[] threadAcquisitions;
public UnfairCounter(int threads) { threadAcquisitions = new int[threads]; }
public synchronized void increment(int threadId) {
count++;
threadAcquisitions[threadId]++;
}
}
// Demonstrate unfair distribution:
int THREADS = 5;
int ITERATIONS = 1_000_000;
UnfairCounter uc = new UnfairCounter(THREADS);
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
final int id = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) uc.increment(id);
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.printf("Total: %d%n", uc.count);
for (int i = 0; i < THREADS; i++) {
System.out.printf("Thread %d acquired lock %d times (%.1f%%)%n",
i, uc.threadAcquisitions[i],
100.0 * uc.threadAcquisitions[i] / (ITERATIONS * THREADS));
}
// Typical output: distribution NOT uniform — some threads get far more acquisitions
// ── Long-running lock holder causes starvation ─────────────────────────
public class LongHolderStarvation {
private final Object lock = new Object();
// This method holds the lock for 500ms — starves all other waiters
public void slowOperation() throws InterruptedException {
synchronized (lock) {
Thread.sleep(500); // holding lock while sleeping — terrible practice
}
}
// These threads wait up to 500ms for each slow operation to complete:
public void fastOperation() {
synchronized (lock) {
// very quick
}
}
}
// ── Thread priority: unreliable, platform-dependent ───────────────────
Thread highPriority = new Thread(() -> {
long count = 0;
while (!Thread.currentThread().isInterrupted()) count++;
System.out.println("High: " + count);
});
Thread lowPriority = new Thread(() -> {
long count = 0;
while (!Thread.currentThread().isInterrupted()) count++;
System.out.println("Low: " + count);
});
highPriority.setPriority(Thread.MAX_PRIORITY); // 10
lowPriority.setPriority(Thread.MIN_PRIORITY); // 1
highPriority.start();
lowPriority.start();
Thread.sleep(1000);
highPriority.interrupt();
lowPriority.interrupt();
// On Linux (default config): both threads get roughly equal CPU time — priority ignored
// On Windows: highPriority may get significantly more CPU time
// DO NOT rely on priority for correctnessFair Locking — ReentrantLock(true) and Thread Pools
// ── Fair ReentrantLock — FIFO ordering ────────────────────────────────
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicLong;
public class FairLockDemo {
// true = fair mode: threads acquire in arrival order
private final ReentrantLock fairLock = new ReentrantLock(true);
private final int[] acquisitions;
public FairLockDemo(int threads) { acquisitions = new int[threads]; }
public void doWork(int threadId) {
fairLock.lock();
try {
acquisitions[threadId]++;
// critical section
} finally {
fairLock.unlock(); // always unlock in finally
}
}
public static void main(String[] args) throws InterruptedException {
int THREADS = 5, ITERS = 100_000;
FairLockDemo demo = new FairLockDemo(THREADS);
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
final int id = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERS; j++) demo.doWork(id);
});
threads[i].start();
}
for (Thread t : threads) t.join();
for (int i = 0; i < THREADS; i++) {
System.out.printf("Thread %d: %d (%.1f%%)%n",
i, demo.acquisitions[i],
100.0 * demo.acquisitions[i] / (ITERS * THREADS));
}
// With fair=true: distribution is approximately equal — ~20% each
// With fair=false: distribution may be uneven — some threads dominate
}
}
// ── Thread pool deadlock (a form of starvation) ───────────────────────
ExecutorService pool = Executors.newFixedThreadPool(2); // only 2 threads
// This task submits another task and WAITS for it — with only 2 pool threads,
// if both threads are running outer tasks, the inner tasks can never start:
Future<Integer> f1 = pool.submit(() -> {
Future<Integer> innerFuture = pool.submit(() -> 42);
return innerFuture.get(); // BLOCKS — waiting for a thread that may not exist
});
// If both pool threads are occupied with outer tasks: inner tasks never run → DEADLOCK
pool.shutdown();
// Fix 1: don't wait for inner tasks synchronously — use callbacks:
pool.submit(() -> {
pool.submit(() -> {
System.out.println("Inner task completed");
});
// Return immediately — don't wait for inner
});
// Fix 2: separate pools for outer and inner tasks:
ExecutorService outerPool = Executors.newFixedThreadPool(4);
ExecutorService innerPool = Executors.newFixedThreadPool(4);
outerPool.submit(() -> {
Future<Integer> innerFuture = innerPool.submit(() -> 42);
return innerFuture.get(); // OK — inner runs in a different pool, never starved
});
// Fix 3: use ForkJoinPool which supports task stealing and managed blocking:
ForkJoinPool fjPool = new ForkJoinPool(4);
fjPool.submit(new RecursiveTask<Integer>() {
@Override protected Integer compute() {
ForkJoinTask<Integer> sub = this.fork(); // ForkJoin: can run sub on idle threads
return sub.join();
}
});
// ── Detecting starvation via thread monitoring ─────────────────────────
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
tmx.setThreadCpuTimeEnabled(true);
// Schedule periodic check:
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
long[] ids = tmx.getAllThreadIds();
for (long id : ids) {
ThreadInfo info = tmx.getThreadInfo(id);
long cpu = tmx.getThreadCpuTime(id); // nanoseconds of CPU time
if (info != null && cpu == 0 && info.getThreadState() == Thread.State.RUNNABLE) {
System.out.printf("Possible starvation: %s has 0 CPU time but is RUNNABLE%n",
info.getThreadName());
}
}
}, 0, 5, TimeUnit.SECONDS);