Better Locks

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.