Protect Shared Data Using ReentrantLock in Java
Problem
A ReentrantLock is an explicit lock: a thread locks it before touching shared data and unlocks it afterwards, and the same thread may lock it again without blocking itself.
Have two threads each make 5,000 deposits of 2 into one shared account, and make sure the final balance is exactly 20,000.
Java Program
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int balance = 0;
void deposit(int amount) {
lock.lock();
try {
balance += amount; // only one thread at a time gets here
} finally {
lock.unlock(); // always released, even if an exception is thrown
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample account = new ReentrantLockExample();
Runnable work = () -> {
for (int i = 0; i < 5_000; i++) account.deposit(2);
};
Thread a = new Thread(work);
Thread b = new Thread(work);
a.start();
b.start();
a.join();
b.join();
System.out.println("Final balance: " + account.balance + " (expected 20000)");
}
}Output
Core Logic
Every update happens between lock() and unlock(), so only one thread at a time can change the balance.
- 1Each account owns a
ReentrantLock. - 2
depositcallslock.lock(), updatesbalance, and unlocks in afinallyblock. - 3Both threads run the same deposit loop at the same time.
- 4After both are joined, the balance is printed.
Key Point: Always unlock in finally — an exception between lock and unlock would otherwise leave the lock held forever.
Key Concepts
Approach 2: tryLock and Reentrancy
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTryLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
lock.lock(); // main holds the lock
Thread other = new Thread(() -> {
boolean got = lock.tryLock(); // doesn't wait: false because main holds it
System.out.println("Other thread got the lock while main held it: " + got);
});
other.start();
other.join();
lock.lock(); // reentrant: the same thread may lock again
System.out.println("Hold count after locking twice: " + lock.getHoldCount());
lock.unlock();
lock.unlock();
System.out.println("Locked after two unlocks: " + lock.isLocked());
}
}
Output
Core Logic
Two features synchronized lacks: tryLock() doesn't wait, and the lock tracks how many times its owner has acquired it.
- 1While main holds the lock, another thread's
tryLock()immediately returnsfalse. - 2Main locks again — allowed, because the lock is reentrant — and the hold count becomes 2.
- 3It must call
unlock()twice before the lock is actually free.
isLocked() returns false.Key Point: Each lock() needs its own matching unlock(); one missing unlock keeps the lock held.
Key Concepts
Interview Questions
When would you choose ReentrantLock over synchronized?
When you need tryLock, a timeout, interruptible waiting, multiple Conditions or a fair lock. For simple mutual exclusion, synchronized is shorter.
What is a fair lock?
new ReentrantLock(true) grants the lock to the longest-waiting thread. It avoids starvation but usually lowers throughput.