Starvation
Starvation
Starvation happens when a thread is repeatedly denied access to a resource it needs because other threads keep getting priority ahead of it. Unlike deadlock, a starved thread is not frozen — it could in principle make progress — it just keeps losing out, sometimes for so long that it might as well be stuck.
What Is Starvation?
Starvation is a fairness failure, not a correctness failure. The starved thread is not broken, not blocked forever in the deadlock sense, and not spinning uselessly like a livelocked thread — it is simply never chosen, over and over, by whatever scheduler or lock is deciding who goes next.
Starvation is related to, but distinct from, livelock — a livelocked thread is actively busy making no progress, while a starved thread can be sitting passively, simply never granted its turn.
Why Thread Safety Matters Here
A starving thread produces no error, no exception, and no obviously wrong output — it just gets slower and slower relative to everything else, until a request that should take milliseconds takes minutes, or never completes within a request timeout at all. Teams following clean architecture will typically catch this only once someone notices a specific customer, or a specific low-priority job type, consistently taking far longer than everyone else — a pattern that is easy to dismiss as noise until it is traced back to an unfair resource allocation policy.
How It Works
The next diagram shows why a strict priority queue can leave one item waiting forever even while the queue stays busy.
Priority queue (highest priority served first):
[Urgent-1 (10)] [Urgent-2 (10)] [Urgent-3 (10)] ... [Report (1)]
^ ^ ^ ^
served served served never reached -
new urgent jobs keep arriving and always outrank the report
The default ReentrantLock provides no ordering guarantee between waiting threads at all — under sustained, uneven contention, a specific thread can, in principle, keep losing out to others indefinitely.
1// File: UnfairLockRisk.java
2// Illustrative only - the exact outcome is not deterministic. An unfair
3// ReentrantLock (the default) gives no ordering guarantee at all between
4// waiting threads, so under sustained contention a particular thread can,
5// in principle, keep losing out to others indefinitely.
6import java.util.concurrent.locks.ReentrantLock;
7
8public class UnfairLockRisk {
9
10 static final ReentrantLock lock = new ReentrantLock();
11
12 static void repeatedlyContend(String label, int attempts) {
13 for (int i = 0; i < attempts; i++) {
14 lock.lock();
15 try {
16 // brief critical section
17 } finally {
18 lock.unlock();
19 }
20 }
21 }
22}This pattern shows up in a handful of recurring places: thread priority misuse, since Thread.setPriority() is only a hint to the operating system's scheduler and low-priority threads can be perpetually preempted by higher-priority ones on platforms that actually honor it; unfair locks under heavy, sustained contention, exactly as UnfairLockRisk illustrates above; custom resource allocators using strict priority ordering with no aging or fairness mechanism, covered in full in this article's real-world example below; and a "greedy" thread that never yields, holding a shared resource for unusually long stretches and leaving little opportunity for others to acquire it.
Code Examples
A ReentrantLock constructed with fair set to true favors the longest-waiting thread instead, giving every waiter a strong, predictable turn.
1// File: FairLockOrderingExample.java
2import java.util.concurrent.locks.ReentrantLock;
3import java.util.concurrent.CopyOnWriteArrayList;
4
5public class FairLockOrderingExample {
6 public static void main(String[] args) throws InterruptedException {
7 ReentrantLock fairLock = new ReentrantLock(true);
8 CopyOnWriteArrayList<String> order = new CopyOnWriteArrayList<>();
9
10 fairLock.lock();
11
12 Thread[] waiters = new Thread[3];
13 for (int i = 0; i < 3; i++) {
14 String name = "Ticket-" + i;
15 waiters[i] = new Thread(() -> {
16 fairLock.lock();
17 try {
18 order.add(name);
19 } finally {
20 fairLock.unlock();
21 }
22 });
23 }
24
25 for (Thread waiter : waiters) {
26 waiter.start();
27 Thread.sleep(50); // let each thread queue up in turn before starting the next
28 }
29
30 Thread.sleep(50);
31 fairLock.unlock();
32
33 for (Thread waiter : waiters) {
34 waiter.join();
35 }
36
37 System.out.println("Service order: " + order);
38 }
39}Output:
Service order: [Ticket-0, Ticket-1, Ticket-2]
Each thread is given a generous 50ms window to fully join the wait queue before the next one starts, and before the lock is released — under those well-separated conditions, a fair lock's queued-waiter ordering is a strong, well-documented practical guarantee, which is exactly why Ticket-0 is served first here, followed by Ticket-1 and Ticket-2 in the order they actually started waiting.
Real-World Example
A print queue always serves the highest-priority job first, and a low-priority report keeps getting pushed back every time a fresh urgent memo arrives — even with a naive aging mechanism meant to eventually boost it.
1// File: PrintJob.java
2
3public class PrintJob {
4 private final String documentName;
5 private int priority;
6
7 public PrintJob(String documentName, int priority) {
8 this.documentName = documentName;
9 this.priority = priority;
10 }
11
12 public String getDocumentName() { return documentName; }
13 public int getPriority() { return priority; }
14
15 public void age() {
16 priority++;
17 }
18}1// File: PrintQueue.java
2import java.util.*;
3
4public class PrintQueue {
5 private final List<PrintJob> jobs = new ArrayList<>();
6
7 public void submit(PrintJob job) {
8 jobs.add(job);
9 }
10
11 public PrintJob selectNext() {
12 PrintJob highest = jobs.stream()
13 .max(Comparator.comparingInt(PrintJob::getPriority))
14 .orElseThrow();
15 jobs.remove(highest);
16 return highest;
17 }
18
19 public void ageWaitingJobs() {
20 jobs.forEach(PrintJob::age);
21 }
22}1// File: PrintQueueStarvationDemo.java
2
3public class PrintQueueStarvationDemo {
4 public static void main(String[] args) {
5 PrintQueue queue = new PrintQueue();
6 PrintJob regularReport = new PrintJob("Monthly-Report", 1);
7 queue.submit(regularReport);
8
9 StringBuilder servedOrder = new StringBuilder();
10
11 for (int round = 0; round < 4; round++) {
12 // A fresh urgent job arrives each round, always outranking the
13 // regular job's current priority unless aging has caught it up
14 queue.submit(new PrintJob("Urgent-Memo-" + round, 10));
15
16 PrintJob served = queue.selectNext();
17 servedOrder.append(served.getDocumentName()).append(" ");
18
19 queue.ageWaitingJobs();
20 }
21
22 System.out.println("Served order: " + servedOrder.toString().strip());
23 }
24}Output:
Served order: Urgent-Memo-0 Urgent-Memo-1 Urgent-Memo-2 Urgent-Memo-3
Monthly-Report starts at priority 1 and only reaches priority 5 after four rounds of +1 aging, still five points behind each fixed priority-10 urgent memo — it never gets served at all in this run. A mistake that appears often in fresher pull requests is implementing an aging fix with too small an increment relative to the priority gap it needs to close, creating the appearance of fairness without actually preventing starvation in practice. A genuinely effective aging scheme needs its increment sized to the real priority range in use, or better, a hard cap on how many times a job can be passed over regardless of relative priority — or a switch to the kind of genuinely fair, FIFO-respecting mechanism this article's FairLockOrderingExample demonstrates.
Best Practices
Prefer a fair lock, a fair semaphore, or an inherently FIFO structure like a queue over strict priority ordering whenever every waiter genuinely needs to be served eventually. ReentrantLock's fairness setting and Semaphore's fairness setting exist for exactly this reason.
Avoid relying on Thread.setPriority() as a scheduling or fairness tool — it is only a hint, honored inconsistently, if at all, depending on the platform and JVM.
If priority-based scheduling is genuinely required, size any aging mechanism to the actual priority range in use, and consider a hard cap on how many times a task can be passed over, rather than trusting a small, arbitrary increment.
Measure the throughput cost of fairness before applying it everywhere — a fair lock is slower than an unfair one due to the bookkeeping strict ordering requires, so it is worth reserving for the specific cases where starvation risk is real.
Common Mistakes
Relying on Thread.setPriority() to prevent starvation overlooks that priority is only a hint to the underlying scheduler — the JLS does not require it to be honored consistently, and many modern JVM and OS combinations give it little to no real effect on scheduling order.
Assuming the default, unfair ReentrantLock provides "good enough" protection against starvation is a second common mistake — an unfair lock can even let a newly-arriving thread acquire it ahead of threads that have already been waiting, a documented behavior sometimes called barging, which can make starvation risk worse rather than better under sustained, uneven contention.
Interview Questions
Q1. What is starvation, and how does it differ from deadlock?
Starvation is when a thread is repeatedly denied access to a resource because other threads keep being favored ahead of it — the thread could in principle make progress, it just keeps losing out. Deadlock is a state where threads are completely frozen, each permanently waiting on a resource another holds. Interviewers listen for whether you clearly separate "never chosen" from "physically stuck."
Q2. How does a fair ReentrantLock help prevent starvation?
It favors granting the lock to the longest-waiting thread rather than any arbitrary waiter, giving every thread a predictable, bounded wait instead of risking indefinite postponement under an unfair lock's unspecified ordering. The nuance being tested is whether you know this comes at a throughput cost, not that fairness is free.
Q3. Is thread priority a reliable way to control scheduling in Java?
No. Thread.setPriority() only provides a hint to the underlying operating system scheduler — the JLS does not require any particular effect, and many platforms give it minimal or no actual influence over which thread runs next. This is a common service-company recall question, but product-company interviewers often follow up asking what you would use instead.
Q4. What is aging, and why can a naive aging implementation still fail to prevent starvation?
Aging gradually increases a waiting task's effective priority the longer it waits, so it eventually outranks competing tasks. A naive implementation can still fail if its increment is too small relative to the priority gap it needs to close, exactly as demonstrated in this article's print queue example, where four rounds of +1 aging still leaves the regular report far behind a fixed priority-10 competitor. Interviewers use this to see if you can spot a fix that looks correct but is actually under-powered.
Q5. Does an unfair lock guarantee that no thread will ever be starved?
No, the opposite — an unfair lock provides no ordering guarantee at all, and can even let a newly-arriving thread acquire the lock ahead of threads that have already been waiting, which is exactly the behavior that creates real starvation risk under sustained contention. The nuance interviewers want is naming "barging" specifically, not just "it's unfair."
Q6. Is there a performance cost to using a fair lock instead of an unfair one?
Yes. A fair lock's strict ordering requires additional bookkeeping and tends to produce more context switches, giving it measurably lower throughput than an unfair lock — fairness is a deliberate tradeoff, not a free upgrade. This question is specifically designed to catch candidates who treat fairness as a strictly better default.
Q7. How does starvation differ from livelock?
A livelocked thread is actively busy, consuming CPU on a retry loop that never succeeds. A starved thread can be sitting passively, simply never granted its turn by a scheduler or lock that keeps favoring other threads — the two share the trait that no progress is made, but for different underlying reasons. Interviewers are listening for the CPU-usage distinction as the concrete, checkable difference.
FAQs
Does starvation always eventually resolve itself?
Not guaranteed — a strict, unbroken priority ordering with no aging or fairness mechanism can, in principle, postpone a low-priority task indefinitely. Some deliberate fairness mechanism is needed to give a bounded guarantee.
Is starvation the same thing as livelock?
No, though they are related liveness problems. Livelock involves threads that are actively busy but make no progress; starvation involves a thread that may simply be waiting passively, repeatedly passed over by a scheduler or lock favoring others.
Can a fair lock or fair semaphore completely eliminate starvation?
For threads that are already waiting in the queue, yes, in practice, quite reliably — but fairness settings generally do not extend the same guarantee to methods like tryLock(), which can still let a thread bypass the queue entirely.
Does the JVM guarantee that Thread.setPriority() has any effect at all?
No. The JLS specifies priority as advisory, and the actual mapping from Java thread priorities to operating system scheduling behavior is entirely platform-dependent — some systems honor it meaningfully, others largely ignore it.
Is starvation more likely with a small thread pool or a large one?
It is generally more likely under sustained heavy contention relative to available capacity, regardless of the pool's exact size — a small pool under heavy load and a large pool under even heavier load can both create the conditions for starvation if the resource allocation policy is not fair.
Can database systems experience starvation the same way Java threads can?
Yes. A transaction repeatedly denied a lock on a row because other transactions keep acquiring it first is the same underlying phenomenon — most database systems include their own fairness or aging mechanisms specifically to prevent this.
Is starvation considered a bug, or an acceptable tradeoff in some systems?
It is generally considered a bug when it affects correctness-critical or user-facing work, but some systems deliberately accept a bounded, well-understood risk of it in exchange for the higher throughput an unfair, priority-based policy can provide — the key distinction is whether the tradeoff was made deliberately and is monitored, rather than being an unnoticed side effect.
Summary
Starvation happens when a thread is repeatedly denied a resource because other threads keep being favored ahead of it — a problem distinct from both deadlock's frozen threads and livelock's busy-but-unproductive ones. A fair lock addresses it directly by favoring the longest-waiting thread, and this article's print queue example shows why a naive aging scheme is not automatically enough on its own — the fix has to be sized to the actual problem.
The habit worth carrying forward from this article is treating Thread.setPriority() as advisory at best, never as a scheduling guarantee, and reaching for a fair lock, a fair semaphore, or a genuinely FIFO structure whenever every competing thread truly needs to be served within some bounded time.
What to Read Next
Learn how threads can signal each other to pause and resume.