ReentrantLock & Condition in Java
tryLock, fairness, lockInterruptibly, always unlock in finally.
An explicit lock
ReentrantLock does what synchronized does, but you acquire and release it. Nothing releases it automatically, so the pattern is always: lock(), then try, with unlock() in finally.
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}Always unlock in finally
lock.lock();
balance -= amount;
if (balance < 0) {
throw new IllegalStateException();
}
lock.unlock();When the exception is thrown, unlock() is skipped. The lock stays held forever and every later caller hangs.
lock.lock();
try {
balance -= amount;
if (balance < 0)
throw new IllegalStateException();
} finally {
lock.unlock();
}finally runs on every path, including exceptions.
The superpowers
tryLock(): acquire only if free right now, otherwise return false at once. tryLock(1, SECONDS): wait a little, then back off, a deadlock escape hatch. lockInterruptibly(): wait, but give up if interrupted. newCondition(): create a wait-set. new ReentrantLock(true): a fair lock.
Your turn
main holds the lock. A second thread calls tryLock(), then main calls it too. What prints?
var lock = new ReentrantLock();
lock.lock();
var t = Thread.ofPlatform().start(() ->
System.out.println(lock.tryLock()));
t.join();
System.out.println(lock.tryLock());false truefalse falsetrue true
Show the answer
The worker gets false immediately: main holds the lock, and tryLock() never waits. main gets true because the lock is reentrant: the owner can acquire it again, raising the hold count to 2. Each lock() needs its own unlock().
Conditions replace wait/notify
lock.newCondition() gives a wait-set with await(), signal() and signalAll(), the explicit-lock version of wait()/notify(). One lock can have several conditions, like notEmpty and notFull. The while loop rule still applies.
lock.lock();
try {
while (queue.isEmpty())
notEmpty.await();
return queue.remove();
} finally {
lock.unlock();
}Fair means faster?
Does a fair ReentrantLock(true) generally give higher throughput than an unfair one?
Think about it, then reveal the answer
No, usually lower. Fairness hands the lock over in arrival order, which costs extra context switches. An unfair lock lets a running thread grab a free lock immediately, which is faster overall. Choose fairness only when starvation is a real problem.
Why teams switched
On Java 21–23, a virtual thread blocking inside synchronized was pinned to its carrier, so many libraries and JDBC drivers replaced synchronized with ReentrantLock. And tryLock with a timeout is the classic way to make a money transfer back off instead of deadlocking.
Key takeaways
- lock(); try { ... } finally { unlock(); }
- tryLock(timeout) lets a thread back off instead of deadlocking
- new ReentrantLock(true) is fair but slower
- Condition.await()/signal() replace wait()/notify()
ArrayBlockingQueue is built from exactly one ReentrantLock and two Conditions, named notEmpty and notFull.
Practice questions
What does this print?
var lock = new ReentrantLock();
lock.lock();
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.isLocked());- 2 true
- 1 false
- 2 false
- 1 true
Check your answer
2 true. The lock is reentrant, so the same thread can acquire it twice; the hold count is 2. One unlock() lowers it to 1, so the lock is still held.
What does this print?
var lock = new ReentrantLock();
lock.lock();
boolean[] got = new boolean[1];
Thread t = new Thread(() -> got[0] = lock.tryLock());
t.start();
t.join();
System.out.println(got[0]);- false
- true
- It hangs forever
- Throws IllegalMonitorStateException
Check your answer
false. Main still holds the lock, so the worker's tryLock() returns false immediately instead of waiting.