ReentrantLock is the most widely used explicit lock in Java.
It gives you the same basic mutual exclusion goal as synchronized, but with a larger control surface around acquisition, waiting, fairness, and conditions.
The name matters too: it is reentrant, which means the thread that already holds the lock can acquire it again safely.
Problem Statement
Suppose an order service updates order state and writes an audit record inside helper methods.
Both methods need the same critical section.
If the lock were not reentrant, a thread could deadlock itself by calling one lock-protected method from another.
What Reentrancy Means
import java.util.concurrent.locks.ReentrantLock;
class OrderService {
private final ReentrantLock lock = new ReentrantLock();
void updateOrder() {
lock.lock();
try {
writeAudit();
} finally {
lock.unlock();
}
}
void writeAudit() {
lock.lock();
try {
System.out.println("Audit written");
} finally {
lock.unlock();
}
}
}
The second lock() works because the same thread already owns the lock.
Important detail:
- each successful acquisition increments the hold count
- each
unlock()decrements it - the lock is not truly released until the hold count returns to zero
Basic Discipline
The essential usage pattern never changes:
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
If you forget the finally block, the code is not “slightly unsafe.”
It is broken under failure paths.
Runnable Example
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
public static void main(String[] args) throws Exception {
OrderWorkflow workflow = new OrderWorkflow();
workflow.processPaidOrder("ORD-101");
}
static final class OrderWorkflow {
private final ReentrantLock lock = new ReentrantLock();
void processPaidOrder(String orderId) {
lock.lock();
try {
System.out.println("Hold count in processPaidOrder = " + lock.getHoldCount());
updateStatus(orderId);
} finally {
lock.unlock();
}
}
void updateStatus(String orderId) {
lock.lock();
try {
System.out.println("Hold count in updateStatus = " + lock.getHoldCount());
sleep(100);
System.out.println("Updated order " + orderId);
} 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 makes the reentrancy visible through the hold count.
What ReentrantLock Adds Beyond synchronized
The practical additions are:
- fairness option
tryLock()- timed acquisition
lockInterruptibly()Condition- inspection helpers such as
isHeldByCurrentThread()andgetHoldCount()
That is why it is often the explicit-lock workhorse in application code.
Production-Style Example
Imagine a bounded API token cache:
- refresh logic needs exclusive mutation
- request paths may skip if the lock is too hot
- shutdown wants blocked maintenance threads to stop waiting
- one condition waits for refresh completion
That is a natural ReentrantLock use case because monitor semantics alone become awkward.
Common Mistakes
- locking in one method and unlocking in another without clear ownership
- forgetting
finally - using
ReentrantLockonly because it feels more advanced thansynchronized - assuming reentrancy means recursive locking is always a good design
Reentrancy is a capability, not a recommendation to create deep lock-heavy call trees.
Key Takeaways
ReentrantLockis an explicit, reentrant mutual exclusion primitive with richer operational behavior thansynchronized.- Reentrancy means the owning thread can reacquire the same lock, but must still release it the same number of times.
- The main reasons to choose it are timed acquisition, interruptible acquisition, fairness, and
Condition. - Always pair acquisition and release in the same logical ownership boundary.
Next post: Fair vs Non Fair Locks in Java
Comments