Skip to Content
Course content

83: ReentrantLock and Explicit Locking

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been using the synchronized keyword for a while, you've probably fallen into the trap of thinking: "ReentrantLock is just a more verbose way of doing the same thing; I'll just stick with synchronized because it's cleaner."

I used to think the same thing. It feels like adding boilerplate for no reason. But the problem with synchronized is that it's an "all or nothing" deal. Once a thread enters a synchronized block and hits a deadlock or a very slow process, that thread is stuck. It will wait forever. There is no "timeout," no "try this and move on," and no way to check if the lock is available without actually blocking the thread.

The Myth: Synchronized is Always the Best Default

Let's look at a classic deadlock scenario: transferring money between two accounts. If Thread A locks Account 1 and tries to lock Account 2, while Thread B locks Account 2 and tries to lock Account 1, your application just... stops. With synchronized, you're dead in the water. You can't tell the thread to "give up" after five seconds.

// This is the 'synchronized' trap
public void transfer(Account from, Account to, double amount) {
    synchronized(from) {
        synchronized(to) { 
            // If two threads do this in reverse order, they deadlock forever.
            from.withdraw(amount);
            to.deposit(amount);
        }
    }
}


In a production system, a deadlock like this is a nightmare because it doesn't throw an exception; the system just stops responding. This is where explicit locking becomes a lifesaver.

Taking Control with Explicit Locks

ReentrantLock is a class in the java.util.concurrent.locks package. Unlike synchronized, which is a language keyword, ReentrantLock is an actual object. This means you have a handle on the lock itself, allowing you to call methods on it to query its state or attempt to acquire it under specific conditions.

The "Reentrant" part of the name just means that if a thread already holds the lock, it can acquire it again without locking itself out. It keeps a hold count, which is exactly how synchronized works under the hood, so you don't lose that behavior.

Avoiding the Forever-Wait with tryLock()

This is the "killer feature" of explicit locking. With tryLock(), you can attempt to acquire the lock and, if it's held by another thread, immediately return false instead of blocking. You can even specify a timeout.

private final ReentrantLock lock = new ReentrantLock();

public void performTask() {
    if (lock.tryLock()) {
        try {
            // We got the lock! Do the work.
        } finally {
            lock.unlock();
        }
    } else {
        // We didn't get the lock. Instead of freezing, 
        // we can log it, try again later, or alert the user.
        System.out.println("System busy, try again in a moment.");
    }
}


Imagine applying this to our bank transfer. Instead of deadlocking, the thread could try to lock both accounts; if it fails to get the second lock within 100 milliseconds, it releases the first lock and tries the whole operation again. The deadlock is broken because the threads are willing to "back off."

The Non-Negotiable Finally Block

Here is the most important piece of advice I can give you regarding ReentrantLock: Always, always put your unlock() call in a finally block.

With synchronized, the JVM automatically releases the lock when the block ends, even if an exception is thrown. With ReentrantLock, the JVM doesn't manage the release. If your code throws a RuntimeException before it hits unlock(), that lock stays held forever. You've just created a manual deadlock that will require a full application restart to fix. I've seen this happen in production environments more times than I care to admit.




📋 Practical Task

Building a Deadlock-Resistant Bank Transfer System

In this exercise, you will implement a SafeBankTransfer class that prevents deadlocks using ReentrantLock. You will move away from synchronized and implement a "try-and-back-off" strategy.

Requirements:

  • Create an Account class with a balance and a ReentrantLock instance.
  • Implement a transfer(Account from, Account to, double amount) method.
  • Inside the transfer method, use tryLock()` with a timeout (e.g., 50 milliseconds) to attempt to acquire locks for both accounts.
  • If both locks are acquired, perform the transfer and then unlock both in a finally block.
  • If either lock cannot be acquired within the timeout, release any locks already held and return false (indicating the transfer failed due to contention).
  • Test your implementation by creating two threads that attempt to transfer money between the same two accounts in opposite directions simultaneously. Verify that the program completes without freezing.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.