1. The “Why”
ReentrantLock is a manual alternative to synchronized. We use it when we need to break Coffman’s Conditions for deadlocks. Specifically:
- Breaking “No Preemption”: With
tryLock(), if a thread can’t get the lock, it can walk away instead of hanging. - Breaking “Uninterruptible Wait”: With
lockInterruptibly(), we can stop a waiting thread externally usingthread.interrupt().
2. Comparison: synchronized vs. ReentrantLock
| Feature | synchronized |
ReentrantLock |
|---|---|---|
| Flexibility | Low (Block-scoped only). | High (Can start lock in one method, end in another). |
| Fairness | No (Random thread gets the lock). | Optional (Can grant lock to the longest-waiting thread). |
| Timeout Support | No (Waits forever). | Yes (tryLock). |
| Interruptibility | No (Cannot be interrupted while waiting). | Yes (lockInterruptibly). |
| Syntax | Simple (Automatic cleanup). | Complex (Requires manual unlock() in a finally block). |
3. The “Golden” Snippet: The Safe Transfer (No Deadlock)
This snippet uses tryLock() to attempt to acquire two locks. If it fails to get the second one, it “preempts” itself by releasing the first one and trying again later.
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class SafeBank {
private final ReentrantLock lockA = new ReentrantLock();
private final ReentrantLock lockB = new ReentrantLock();
public void transferMoney() {
Random random = new Random();
while (true) {
boolean gotLockA = lockA.tryLock();
boolean gotLockB = lockB.tryLock();
if (gotLockA && gotLockB) {
try {
System.out.println(Thread.currentThread().getName() + " acquired both locks!");
// Critical Section: Perform Transfer
break;
} finally {
lockA.unlock();
lockB.unlock();
}
}
// If we only got one, we MUST release it to avoid holding and waiting
if (gotLockA) lockA.unlock();
if (gotLockB) lockB.unlock();
// Sleep a bit before retrying to avoid "Livelock"
try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { return; }
}
}
}
Code Explanation:
tryLock(): Unlike a normal lock, this returnstrueimmediately if the lock is available andfalseif it isn’t. It never blocks your thread.finally { lock.unlock() }: This is MANDATORY. BecauseReentrantLockisn’t bound by{}brackets likesynchronized, if you forget to unlock, the lock stays held until the app crashes.- Livelock Prevention: We sleep for a random amount of time. If two threads both release their locks and retry at the exact same microsecond, they might keep bumping into each other forever (Livelock). Randomness breaks this cycle.
Example Output:
Thread-1: Failed to get Lock B, releasing Lock A...
Thread-2: Failed to get Lock A, releasing Lock B...
Thread-1: Acquired both locks!
Thread-2: Acquired both locks!
4. The Gotchas
- The “Forgotten Unlock”: If your code throws an exception inside the critical section and you didn’t put
unlock()in afinallyblock, the lock is never released. Other threads will be blocked forever. - Overhead:
ReentrantLockis slightly more memory-intensive thansynchronized. Don’t use it unless you actually need features liketryLockorfairness. - The
lock()vstryLock(): If you calllock.lock(), it behaves exactly likesynchronized(it blocks). OnlytryLock()provides the non-blocking behavior.