ReentrantLock like its name suggests, is reentrant; Allowing the same thread to acquire the same lock multiple times!
Code example of reentrant behaviour
class Main { void main() { ThreadSafeCounter counter = new ThreadSafeCounter(); new Thread(()-> counter.increment()).start(); new Thread(()-> counter.incrementTwice()).start(); }}class ThreadSafeCounter { private int count = 0; private ReentrantLock lock = new ReentrantLock(true); public void increment() { lock.lock(); // Aquire lock try { count++; } finally { // Always use finally to prevent deadlocks lock.unlock(); } } public void incrementTwice() { lock.lock(); // First lock acquisition try { System.out.println("incrementTwice() acquired lock"); increment(); // Calls increment() which locks AGAIN increment(); // Same thing System.out.println("incrementTwice() releasing lock"); } finally { lock.unlock(); } }}
Fairness
When a thread fails to acquire the lock, it goes into the waiting state and joins the Entry/Sync Queue of the lock. This Sync Queue is implemented with AbstractQueuedSynchronizer, so it guarantees FIFO ordering.
Yet, the constructor still has an optional fairness parameter which is set to false by default:
unfair: new ReentrantLock() or new ReentrantLock(false)
fair: new ReentrantLock(true)
Even though the Sync Queue is already FIFO ordered which would mean that it is intrinsically fair, the default “unfair” mode, allows for “barging”.
Barging is when a running thread skips ahead of the waiting threads in the Sync Queue and acquires the lock. Barging is possible in “unfair” mode because ReentrantLock doesn’t check if there are threads in the Sync Queue before issuing the lock.
Whereas when initialised to be “fair”, the ReentrantLock checks the Sync Queue before issuing the lock, preventing barging.
Enabling fairness helps to reduce the probability of thread starvation but comes with a slight performance penalty.
It doesn’t completely prevent thread starvation because ultimately that depends on the OS thread scheduler. If the scheduler starves a thread before it can attempt to acquire a lock, theres nothing that lock ordering can do to prevent starvation.
An important caveat, is that the tryLock() method is explicitly allowed to barge in both the fair and unfair implementations of AbstractQueuedSynchronizer in the ReentrantLock.
Snippet of ReentrantLock's constructors.
FairSync & NonfaitSync are implementations of AbstractQueuedSynchronizer
public ReentrantLock() { sync = new NonfairSync();}public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
Multiple Condition Queues
ReentrantLock supports multiple “wait queues” via the Condition interface (similar to wait/notify but more flexible).
Fairness only applies within each queue, not between queues.
Intra-queue fairness: yes! (within a single condition queue)
Inter-queue fairness: no! (between different condition queues)
Thread States
Built on AbstractQueuedSynchronizer (AQS) using park/unpark mechanisms.
Threads waiting for locks are in WAITING or TIMED_WAITING states (NEVER BLOCKED).