Thread Starvation: Why Threads Starve and How to Fix It
This article explains thread starvation in Java concurrency, demonstrates it with a code example showing unfair lock acquisition and cumulative wait times, and provides two solutions: moving slow operations outside synchronized blocks and using ReentrantLock tryLock with timeout to bound waiting.
What Is Thread Starvation
Thread starvation occurs when a thread is perpetually denied access to resources it needs, such as CPU time or a lock, due to scheduling policies or synchronization design flaws. The thread is ready and willing to run but cannot proceed because other threads continuously acquire the resource first.
Code Reproduction of Starvation
The author creates four threads that arrive 100 ms apart. Each thread executes a simulated slow SQL query (3 seconds) inside a synchronized block that also writes to a shared lastResult variable. Because the lock is held for the entire query, later-arriving threads must wait for all preceding threads to finish.
public class StarvationBroken {
private static final Object LOCK = new Object();
private static String lastResult = "";
public static void main(String[] args) throws Exception {
long bootAt = System.currentTimeMillis();
Thread[] workers = new Thread[4];
for (int i = 0; i < 4; i++) {
final int id = i + 1;
workers[i] = new Thread(() -> {
sleepMs(id * 100L);
long arriveAt = System.currentTimeMillis();
System.out.println("[" + elapsed(bootAt) + "ms] worker-" + id + ": arrived");
synchronized (LOCK) {
long waitMs = System.currentTimeMillis() - arriveAt;
System.out.println("[" + elapsed(bootAt) + "ms] worker-" + id + ": got lock after " + waitMs + "ms waiting");
sleepMs(3000);
lastResult = "result-" + id;
}
System.out.println("[" + elapsed(bootAt) + "ms] worker-" + id + ": done");
}, "worker-" + id);
workers[i].start();
}
for (Thread t : workers) t.join();
System.out.println("all done in " + elapsed(bootAt) + "ms, lastResult=" + lastResult);
}
private static long elapsed(long bootAt) { return System.currentTimeMillis() - bootAt; }
private static void sleepMs(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Output Analysis
Running on Windows 11 with Java 17 produced:
[108ms] worker-1: arrived
[113ms] worker-1: got lock after 5ms waiting
[209ms] worker-2: arrived
[308ms] worker-3: arrived
[410ms] worker-4: arrived
[3128ms] worker-4: got lock after 2718ms waiting
[3128ms] worker-1: done
[6141ms] worker-4: done
[6141ms] worker-3: got lock after 5833ms waiting
[9158ms] worker-3: done
[9158ms] worker-2: got lock after 8949ms waiting
[12173ms] worker-2: done
all done in 12173ms, lastResult=result-2Key observations:
Unfair acquisition: Threads arrived in order 1,2,3,4 but acquired the lock in order 1,4,3,2. Worker-2 (second to arrive) waited 8.9 seconds — the "unlucky thread".
Cumulative wait: Each extra millisecond inside the lock adds one millisecond of wait for every thread still queued. Worker-1 held 3s → worker-4 waited 2.7s; worker-4 held 3s → worker-3 waited 5.8s; worker-3 held 3s → worker-2 waited 8.9s.
Total time inflated: Four independent 3-second tasks took 12 seconds sequentially due to the coarse-grained lock.
Solution 1: Reduce Lock Scope
Only the write to lastResult requires mutual exclusion. The slow query can run outside the lock. The fixed version moves the 3-second sleep before the synchronized block:
public class StarvationFixed {
private static final Object LOCK = new Object();
private static String lastResult = "";
public static void main(String[] args) throws Exception {
long bootAt = System.currentTimeMillis();
Thread[] workers = new Thread[4];
for (int i = 0; i < 4; i++) {
final int id = i + 1;
workers[i] = new Thread(() -> {
sleepMs(id * 100L);
System.out.println("[" + elapsed(bootAt) + "ms] worker-" + id + ": arrived, start query");
sleepMs(3000);
String result = "result-" + id;
long askAt = System.currentTimeMillis();
synchronized (LOCK) {
lastResult = result;
}
long waitMs = System.currentTimeMillis() - askAt;
System.out.println("[" + elapsed(bootAt) + "ms] worker-" + id + ": updated, lock waiting " + waitMs + "ms");
}, "worker-" + id);
workers[i].start();
}
for (Thread t : workers) t.join();
System.out.println("all done in " + elapsed(bootAt) + "ms, lastResult=" + lastResult);
}
private static long elapsed(long bootAt) { return System.currentTimeMillis() - bootAt; }
private static void sleepMs(long ms) {
try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}Output shows zero lock wait for all threads and total time drops to ~3.4 seconds:
[114ms] worker-1: arrived, start query
[214ms] worker-2: arrived, start query
[314ms] worker-3: arrived, start query
[414ms] worker-4: arrived, start query
[3130ms] worker-1: updated, lock waiting 0ms
[3230ms] worker-2: updated, lock waiting 0ms
[3315ms] worker-3: updated, lock waiting 0ms
[3415ms] worker-4: updated, lock waiting 0ms
all done in 3415ms, lastResult=result-4The lock still protects the shared write, but its critical section is now milliseconds instead of seconds.
Solution 2: Bound Waiting with ReentrantLock tryLock
When the slow operation cannot be moved out of the lock, use ReentrantLock.tryLock(timeout, unit) to set a maximum wait time:
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// critical section
} finally {
lock.unlock();
}
} else {
// fallback: return default data, log, or execute degradation logic
}If the lock isn't acquired within the timeout, the thread can proceed with a fallback instead of blocking indefinitely.
Additional Check: Infinite Loops Inside Locks
Verify that no infinite loops exist within synchronized blocks or lock sections; a loop that never terminates will never release the lock, causing permanent starvation for all other threads.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
samdeepthink
Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
