Fairness sounds like an obvious good, but in locking it comes with real cost.
Java lets you choose whether a ReentrantLock should prefer first-waiting threads or allow opportunistic acquisition by newly arriving threads.
That choice can change:
- throughput
- tail latency
- starvation risk
- scheduling predictability
So fairness is not a moral preference. It is a workload trade-off.
Problem Statement
Suppose many threads compete for one lock protecting a hot resource.
If the lock is non-fair, a newly arriving thread may “barge” ahead of older waiters.
That can improve throughput. But if a few unlucky threads keep losing the race, long waits and starvation become more likely.
A fair lock tries to reduce that.
Non-Fair Lock
The default ReentrantLock is non-fair.
That means:
- a thread that arrives at the right moment may acquire the lock immediately
- an older waiting thread may not always be the next winner
This often improves throughput because the handoff is less strict and the scheduler can make opportunistic progress.
That is why non-fair is the default.
Fair Lock
import java.util.concurrent.locks.ReentrantLock;
ReentrantLock fairLock = new ReentrantLock(true);
A fair lock tries to grant access in arrival order.
That can reduce starvation risk and improve predictability for waiting threads.
But strict ordering usually costs throughput under contention.
Why the Trade-Off Exists
Fairness often means:
- more orderly handoff
- less barging by late-arriving threads
- better predictability under contention
But it also often means:
- more scheduling overhead
- less opportunistic progress
- lower aggregate throughput
So the correct choice depends on workload goals, not ideology.
Runnable Example
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
public static void main(String[] args) throws Exception {
ReentrantLock lock = new ReentrantLock(true);
for (int i = 1; i <= 4; i++) {
String workerName = "worker-" + i;
Thread thread = new Thread(() -> runTask(lock, workerName), workerName);
thread.start();
TimeUnit.MILLISECONDS.sleep(30);
}
}
static void runTask(ReentrantLock lock, String workerName) {
lock.lock();
try {
System.out.println(workerName + " acquired lock");
sleep(100);
} finally {
lock.unlock();
}
}
static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
This demo only illustrates the policy shape. Real fairness decisions should come from profiling and latency behavior, not from a toy program.
Production-Style Scenarios
Fair locks can help when:
- starvation has actually been observed
- long waits for specific task classes are unacceptable
- you care more about predictable access than raw throughput
Non-fair locks usually remain better when:
- throughput dominates
- contention is moderate
- starvation is not a real observed problem
The real question is:
- do you need fairness enough to pay for it?
Common Mistakes
- enabling fairness because it sounds more correct
- ignoring latency distribution and looking only at average throughput
- trying to solve application-level prioritization problems only with fair locking
- expecting fair locks to fix poor executor design or oversized critical sections
Fairness is one scheduling lever. It is not a complete workload-management strategy.
Decision Guide
Prefer non-fair locking when:
- throughput is the main goal
- starvation is not observed
- you want the default and usually best general-purpose behavior
Consider fair locking when:
- some threads are waiting too long in practice
- predictability matters more than peak throughput
- you have measured that the fairness cost is acceptable
Use data, not instinct.
Key Takeaways
- Fair locks reduce starvation risk by preferring older waiters.
- Non-fair locks usually deliver better throughput.
- The right choice depends on observed workload behavior, especially contention and tail latency.
- Enable fairness when starvation or unacceptable waiting is a real problem, not by default.
Next post: tryLock and Timed Lock Acquisition in Java
Comments