Java Programs›Multithreading›Protect Shared Data Using ReentrantLock

Protect Shared Data Using ReentrantLock in Java

intermediate·  Multithreading  ·  Concurrency

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.

Input
2 threads × 5,000 deposits of 2
Output
20000

Java Program

Java
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

Final balance: 20000 (expected 20000)

Core Logic

Every update happens between lock() and unlock(), so only one thread at a time can change the balance.

How It Works
  1. 1Each account owns a ReentrantLock.
  2. 2deposit calls lock.lock(), updates balance, and unlocks in a finally block.
  3. 3Both threads run the same deposit loop at the same time.
  4. 4After both are joined, the balance is printed.
10,000 deposits of 2 always total 20,000, however the two threads interleave.
💡

Key Point: Always unlock in finally — an exception between lock and unlock would otherwise leave the lock held forever.

Key Concepts

ReentrantLocklock()unlock()try-finally

Approach 2: tryLock and Reentrancy

Java
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

Other thread got the lock while main held it: false Hold count after locking twice: 2 Locked after two unlocks: false

Core Logic

Two features synchronized lacks: tryLock() doesn't wait, and the lock tracks how many times its owner has acquired it.

How It Works
  1. 1While main holds the lock, another thread's tryLock() immediately returns false.
  2. 2Main locks again — allowed, because the lock is reentrant — and the hold count becomes 2.
  3. 3It must call unlock() twice before the lock is actually free.
After two unlocks, isLocked() returns false.
💡

Key Point: Each lock() needs its own matching unlock(); one missing unlock keeps the lock held.

Key Concepts

tryLock()getHoldCount()reentrancy

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.

Learn the Concept

Related Programs