Avoiding Thread Starvation in Java with ReentrantLock


Introduction
In multithreaded applications, one of the most common issues developers face is thread starvation. Starvation happens when some threads are consistently denied access to a shared resource because other threads monopolize it. Over time, the “weaker” threads may never get a chance to run, which leads to unfair execution and poor system performance.
In this article, we’ll first look at a simple example of thread starvation in Java. Then, we’ll explore how to solve this problem using the ReentrantLock
with fairness enabled.
The Problem: Thread Starvation
Consider the following example:
class TaskHandler {
public synchronized void executeTask() {
String threadName = Thread.currentThread().getName();
long sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += (i * 3) % 7;
}
System.out.println("Thread " + threadName + " end the calc with the result: " + sum);
while(true) {
System.out.println(">> " + threadName + " keeps running and holding the lock...");
}
}
}
public class StarvationProblem {
public static void main(String[] args) {
TaskHandler handler = new TaskHandler();
for (int i = 1; i <= 10; i++) {
Thread t = new Thread(() -> handler.executeTask(), "Thread-" + i);
t.start();
}
}
}
What happens here?
We create a shared
TaskHandler
with asynchronized
method.Each thread tries to run
executeTask()
.However, once the first thread enters the method, it performs a calculation and then enters an infinite loop, never releasing the lock.
As a result, only that single thread continues running, and the remaining nine threads are indefinitely blocked — a clear case of starvation.
The output looks something like this:
Thread-1 keeps running and holding the lock...
Thread-1 keeps running and holding the lock...
Thread-1 keeps running and holding the lock...
...
The Solution: ReentrantLock with Fairness
To prevent starvation, Java provides a more flexible synchronization mechanism: the ReentrantLock
. Unlike synchronized
, a ReentrantLock
can be configured to use a fair locking policy.
Here’s the improved code:
import java.util.concurrent.locks.ReentrantLock;
public class StarvationSolutionWithReentrantLock {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(true); // fairness enabled
Thread lowPriorityThread = new Thread(() -> {
while (true) {
lock.lock();
try {
System.out.println("Low priority thread running...");
} finally {
lock.unlock();
}
}
});
Thread highPriorityThread = new Thread(() -> {
while (true) {
lock.lock();
try {
System.out.println("High priority thread running...");
} finally {
lock.unlock();
}
}
});
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
lowPriorityThread.start();
highPriorityThread.start();
}
}
Why does this work?
The key is the constructor:
new ReentrantLock(true);
Passing
true
enables fair locking, which means threads acquire the lock in a first-come, first-served (FIFO) order.Unlike the unfair default mode, fairness ensures that no thread (even low-priority ones) gets starved out.
Both high- and low-priority threads get their turn to run, according to the order they requested the lock.
Conclusion
Starvation is a subtle but dangerous problem in concurrent programming. Using synchronized
methods alone can easily lead to scenarios where some threads never get a chance to execute.
By switching to ReentrantLock
with fairness enabled, we ensure fair access to resources, prevent starvation, and make our multithreaded applications more reliable and predictable.
Whenever you design concurrent systems where multiple threads compete for limited resources, consider whether fair locks are necessary to maintain balance.
Subscribe to my newsletter
Read articles from Luis Gustavo Souza directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
