Priya fixed the sold-count race with synchronized. It works. But her kitchen — the inventory service — has new problems. Sometimes a worker just needs to peek at stock without blocking everyone. Sometimes a price update must wait, but not forever. And during peak hours, hundreds of students read the menu while only one person updates prices. synchronized treats every visitor the same. Priya needs better locks.
Why explicit locks?
synchronized is simple, but it cannot:
- Try to acquire a lock without blocking forever.
- Wait only a set amount of time.
- Be interrupted while waiting.
- Guarantee fair ordering (first come, first served).
- Let many readers proceed at once.
ReentrantLock, ReadWriteLock, and StampedLock fill these gaps.
ReentrantLock basics
Same idea as synchronized — one thread at a time — but you control it explicitly:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ALWAYS unlock in finally
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
If an exception flies out of the try block, finally still runs. Without unlock(), the lock stays held forever and every other thread stalls.
Key points:
- Always unlock in
finally. - Reentrant — the same thread can lock multiple times; must unlock the same number of times.
tryLock — do not wait forever
QuickCart’s kitchen board: if the lock is busy, skip and retry later instead of standing idle:
public boolean tryRecordSale() {
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // Lock not available — try again later
}
tryLock with timeout
Wait up to N seconds:
public boolean tryRecordSaleWithTimeout() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // Timed out
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
This is something synchronized simply cannot do.
lockInterruptibly — stop waiting when interrupted
public void incrementInterruptibly() throws InterruptedException {
lock.lockInterruptibly(); // Can be interrupted while waiting
try {
count++;
} finally {
lock.unlock();
}
}
Useful when a thread should stop waiting for a lock if another thread interrupts it.
Fair locks
By default, ReentrantLock is non-fair — better performance, but no guarantee of order.
// Fair lock — threads acquire in arrival order (slower but predictable)
private final ReentrantLock lock = new ReentrantLock(true);
// Non-fair lock (default) — faster, no order guarantee
private final ReentrantLock lock = new ReentrantLock(false);
Use fair locks when starvation (one thread never getting the lock) is a real concern.
Lock status
if (lock.isHeldByCurrentThread()) {
// Current thread holds the lock
}
if (lock.isLocked()) {
// Some thread holds the lock
}
int waiters = lock.getQueueLength(); // Threads waiting for this lock
ReadWriteLock — many readers, one writer
Priya’s product catalog is read constantly but updated rarely. With synchronized, every reader blocks every other reader. Wasteful.
ReadWriteLock splits the lock in two:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ProductCatalog {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private String priceList = "Hoodie: Rs 799";
public String readPriceList() {
lock.readLock().lock();
try {
return priceList; // Many readers at once — OK
} finally {
lock.readLock().unlock();
}
}
public void updatePriceList(String newList) {
lock.writeLock().lock();
try {
priceList = newList; // Only one writer, no readers
} finally {
lock.writeLock().unlock();
}
}
}
Rules
- Read lock — many threads can hold it at the same time.
- Write lock — only one thread, and no readers while it is held.
- You cannot upgrade a read lock to a write lock on
ReentrantReadWriteLock— that causes deadlock.
tryLock with ReadWriteLock
public String tryReadPriceList() {
if (lock.readLock().tryLock()) {
try {
return priceList;
} finally {
lock.readLock().unlock();
}
}
return null; // Lock not available
}
StampedLock — optimistic reads
For very read-heavy data, StampedLock adds optimistic reading. The reader does not block writers at first — it reads, then checks whether a write snuck in:
import java.util.concurrent.locks.StampedLock;
public class ProductCatalog {
private final StampedLock lock = new StampedLock();
private String priceList = "Hoodie: Rs 799";
public String readPriceList() {
long stamp = lock.tryOptimisticRead();
String current = priceList;
if (!lock.validate(stamp)) {
// A write happened — fall back to a real read lock
stamp = lock.readLock();
try {
current = priceList;
} finally {
lock.unlockRead(stamp);
}
}
return current;
}
public void updatePriceList(String newList) {
long stamp = lock.writeLock();
try {
priceList = newList;
} finally {
lock.unlockWrite(stamp);
}
}
}
Optimistic reads are faster when writes are rare. If a write did happen, you re-read with a proper lock.
Upgrade read to write (StampedLock only)
Unlike ReentrantReadWriteLock, StampedLock can sometimes upgrade:
public void upgradeAndWrite(String newList) {
long stamp = lock.readLock();
try {
String current = priceList;
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp != 0) {
stamp = writeStamp;
priceList = newList;
} else {
lock.unlockRead(stamp);
stamp = lock.writeLock();
priceList = newList;
}
} finally {
lock.unlock(stamp);
}
}
StampedLock is more complex. Reach for it when profiling shows read contention is a real bottleneck — not on day one.
synchronized vs ReentrantLock
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Timeout | No | Yes (tryLock) |
| Interrupt while waiting | No | Yes (lockInterruptibly) |
| Try-lock | No | Yes |
| Fair ordering | No | Optional (new ReentrantLock(true)) |
| Multiple conditions | No | Yes |
| Must unlock in same method | Automatic | Manual — use finally |
| Performance | Slightly faster for simple cases | Slightly slower |
Use synchronized for simple, short critical sections.
Use ReentrantLock when you need try-lock, timeout, interrupt, or fairness.
Rules that prevent pain
Always unlock in finally:
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
Never hold a lock during slow I/O:
// BAD — blocks every other thread for the whole API call
lock.lock();
try {
paymentGateway.charge(card); // slow network call
} finally {
lock.unlock();
}
// GOOD — lock only around the shared data update
PaymentResult result = paymentGateway.charge(card);
lock.lock();
try {
updateOrderStatus(result);
} finally {
lock.unlock();
}
Never unlock without locking first — you get IllegalMonitorStateException.
Document lock ordering to prevent deadlock:
/**
* Lock order: accountA -> accountB (always by lower account ID first)
*/
QuickCart inventory service
Priya combines read-heavy catalog access with guarded writes:
public class InventoryService {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<String, Integer> stock = new HashMap<>();
public int getStock(String productId) {
lock.readLock().lock();
try {
return stock.getOrDefault(productId, 0);
} finally {
lock.readLock().unlock();
}
}
public boolean tryReserve(String productId) {
if (!lock.writeLock().tryLock()) {
return false; // Busy — tell customer to retry
}
try {
int current = stock.getOrDefault(productId, 0);
if (current <= 0) return false;
stock.put(productId, current - 1);
return true;
} finally {
lock.writeLock().unlock();
}
}
public void restock(String productId, int amount) {
lock.writeLock().lock();
try {
stock.put(productId, stock.getOrDefault(productId, 0) + amount);
} finally {
lock.writeLock().unlock();
}
}
}
Students read stock freely. Reservations grab the write lock — with tryLock so a hot product does not freeze the whole shop.
ReentrantLock for order processing
When Priya needs timeout on a critical section:
public class OrderLockService {
private final ReentrantLock lock = new ReentrantLock();
public boolean processWithTimeout(String orderId) {
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
processOrder(orderId);
return true;
} finally {
lock.unlock();
}
}
return false; // Could not get lock in time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
private void processOrder(String orderId) {
// Update shared order state...
}
}
What to remember
- ReentrantLock — try-lock, timeout, interrupt, fair option; always unlock in finally.
- ReadWriteLock — many readers OR one writer; great for catalogs and caches.
- StampedLock — optimistic reads for heavy read workloads; can upgrade read to write with care.
- Prefer synchronized when you do not need the extra features.
- Never hold locks during slow network or disk calls.
- Never forget to unlock — other threads wait forever.
What Priya does next: with threads, pools, futures, pipelines, and safe shared data, QuickCart is ready for the next campus sale — and Priya finally gets to watch the orders roll in instead of fixing the counter.