⚡ Advanced Concurrency · Advanced

ReentrantLock & Condition in Java

tryLock, fairness, lockInterruptibly, always unlock in finally.

🧩 The mysterysynchronized waits forever and never takes no for an answer. ReentrantLock can try, give up, be interrupted and play fair. The price: you must remember to unlock it.

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

✗ Leaks the lock
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.

✓ Always released
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.

🔮 Predict it

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());
  1. false true
  2. false false
  3. true 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();
}
🤔 Think first

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.

💼 In the real world

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

  1. lock(); try { ... } finally { unlock(); }
  2. tryLock(timeout) lets a thread back off instead of deadlocking
  3. new ReentrantLock(true) is fair but slower
  4. Condition.await()/signal() replace wait()/notify()
🤯 Did you know?

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());
  1. 2 true
  2. 1 false
  3. 2 false
  4. 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]);
  1. false
  2. true
  3. It hangs forever
  4. Throws IllegalMonitorStateException
Check your answer

false. Main still holds the lock, so the worker's tryLock() returns false immediately instead of waiting.

Next: a cache read 10,000 times a second and written once a minute. Why should readers wait for each other? Read-write locks.