Definition
- Fair mode –
new ReentrantLock(true): grants ownership in FIFO order so the longest-waiting thread is served next. - Unfair mode –
new ReentrantLock()(default): allows barging, where a newly arriving thread may grab the lock ahead of queued threads. - Throughput trade-off – unfair avoids the context-switch cost of waking a parked thread, delivering higher throughput under contention.
- tryLock() subtlety – with no timeout,
tryLock()always barges even on a fair lock; usetryLock(timeout, unit)for fair behaviour. - synchronized – always unfair;
ReentrantLock(true)is the only JDK tool for fair mutual exclusion.
At National Bank of India, a fair lock is a strict token queue: customers take a numbered token and are served in exact order. An unfair lock is a free-for-all at the counter: whoever is closest jumps in. The free-for-all serves more per hour but an unlucky customer could wait a very long time.
Understanding the Concepts
How do you choose fair or unfair mode?
The choice is made at construction time and cannot be changed later.
package com.bank.locks;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
// Fair: strict FIFO ordering, prevents starvation
private final ReentrantLock fair = new ReentrantLock(true);
// Unfair (default): allows barging, higher throughput
private final ReentrantLock unfair = new ReentrantLock();
}
How does fair mode work internally?
When a thread calls lock() on a fair lock and the lock is held, it joins the tail of a FIFO queue managed by AbstractQueuedSynchronizer (AQS). When the lock is released, the thread at the head of the queue is granted ownership. A new thread arriving while others are waiting will not jump the queue.
package com.bank.service;
import java.math.BigDecimal;
import java.util.concurrent.locks.ReentrantLock;
public class FairTransferService {
private final ReentrantLock lock = new ReentrantLock(true); // fair
private BigDecimal balance = new BigDecimal("100000.00");
public void debit(BigDecimal amount) {
lock.lock();
try {
balance = balance.subtract(amount);
} finally {
lock.unlock();
}
}
}
How does unfair barging work?
When the lock is released, a newly arriving thread may attempt a CAS to grab the lock immediately, before the head of the wait queue is woken. This is called barging. It is faster because waking a parked thread costs a context switch (typically 5-10 microseconds), while a CAS on the same core costs nanoseconds.
What is the tryLock subtlety?
tryLock() with no timeout always barges, even on a fair lock. This is documented behavior. If you need tryLock to respect fairness, use the timed variant.
// Barges even on a fair lock:
if (fair.tryLock()) {
try { /* ... */ } finally { fair.unlock(); }
}
// Respects fairness (joins the FIFO queue):
if (fair.tryLock(100, TimeUnit.MILLISECONDS)) {
try { /* ... */ } finally { fair.unlock(); }
}
How do fair and unfair compare?
| Aspect | Fair | Unfair |
|--------|------|--------|
| Ordering | Strict FIFO | Barging allowed |
| Starvation | Prevented | Possible |
| Throughput | Lower (context switch per handoff) | Higher (barging avoids wake cost) |
| Default | No | Yes |
| synchronized equivalent | None | Always unfair |
| Best for | SLA compliance, audit threads | General performance |
When should you use fair mode?
Only when starvation is a real, observed problem in production. For example, at National Bank of India, a storm of high-frequency settlement threads starved the nightly audit thread that needed the same lock. Switching to fair mode guaranteed the audit thread was served in order.
What is starvation and how do you detect it?
Starvation is when a thread is perpetually denied access to a resource because other threads keep acquiring it first. In thread dumps, a starved thread appears as WAITING on a lock for extended periods while other threads repeatedly acquire and release the same lock. JFR ThreadPark events with long durations on the same lock indicate starvation.
What is priority inversion?
Priority inversion occurs when a high-priority thread waits on a lock held by a low-priority thread, and the low-priority thread is preempted by medium-priority threads. The high-priority thread is effectively blocked by medium-priority work. Real-time operating systems solve this with priority inheritance, but Java does not have this mechanism.
What are common mistakes?
Using fair mode everywhere "just to be safe" reduces throughput significantly under contention. Fair mode should be the exception, not the default. Another mistake is forgetting that tryLock() without timeout barges on fair locks, which can violate fairness expectations.
What are common interview follow-up questions?
- How does the AQS (AbstractQueuedSynchronizer) implement fairness? Fair mode checks whether there are queued threads before attempting to acquire; unfair mode attempts CAS immediately.
- Can starvation happen with synchronized? Yes, since synchronized is always unfair.
- How does StampedLock handle fairness? StampedLock does not support fairness mode.
Visual Overview
Fair Lock (FIFO): Queue: [A] ← [B] ← [C] ← new thread D Lock freed → A gets it (head of queue) D joins the tail, served last Unfair Lock (Barging): Queue: [A] ← [B] ← [C] Lock freed → D arrives and barges in D gets the lock immediately A, B, C continue waiting Throughput: Fair: │▓▓▓▓░░▓▓▓▓░░▓▓▓▓│ context switch gaps Unfair: │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│ no gaps, higher throughput
Key Takeaway
Lock fairness is a mode chosen at construction. Fair locks grant ownership in FIFO order to prevent starvation but pay a context-switch cost on every handoff. Unfair locks allow barging for higher throughput.
I default to unfair for performance and switch to fair only when a thread is genuinely being starved, as we observed with our audit thread at National Bank of India.
Remember one sentence: fair locks trade throughput for ordering to prevent starvation, unfair locks trade ordering for throughput, and I default to unfair unless starvation is a real, observed problem.
---