Definition
- Coarse-grained – one lock protects ALL shared state. Simple to implement. Impossible to deadlock (only one lock). But low concurrency: all threads serialize through one bottleneck.
- Fine-grained – separate lock per resource (per account, per table row). High concurrency: operations on different resources run in parallel. Risk: deadlock when acquiring multiple fine-grained locks in different orders.
- Lock ordering – always acquire locks in a deterministic global order (by account ID, by hash). This breaks the circular-wait condition of deadlock, making fine-grained locking safe.
- Trade-off – coarse = simple + safe + slow. Fine + ordering = complex + fast + safe. Lock-free (Atomic, CAS) = maximum concurrency + highest complexity.
Coarse locking is one key for the entire vault at National Bank of India. Only one teller enters at a time. Simple, but 200 tellers queue up for one key. Fine-grained locking is a separate key for each account drawer. Two hundred tellers access 200 different drawers simultaneously. But Rajesh locks drawer-A then needs drawer-B, while Priya locks drawer-B then needs drawer-A: deadlock. The rule "always open the lower-numbered drawer first" prevents circular waiting.
Understanding the Concepts
What is coarse-grained locking?
Coarse-grained locking uses a single lock to protect all shared state. Every operation acquires the same lock, so only one thread can execute at a time.
package com.bank.concurrency;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
public class CoarseBank {
private final Object bankLock = new Object();
private final Map<String, BigDecimal> accounts = new HashMap<>();
public void transfer(String from, String to, BigDecimal amount) {
synchronized (bankLock) {
BigDecimal fromBal = accounts.getOrDefault(from, BigDecimal.ZERO);
BigDecimal toBal = accounts.getOrDefault(to, BigDecimal.ZERO);
accounts.put(from, fromBal.subtract(amount));
accounts.put(to, toBal.add(amount));
}
}
// Simple! No deadlock! But SAV2001→CUR3001 blocks FD4001→SAV5001
// even though they touch different accounts
}
What is fine-grained locking?
package com.bank.concurrency;
import java.math.BigDecimal;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class FineBank {
private final ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, BigDecimal> accounts = new ConcurrentHashMap<>();
private ReentrantLock getLock(String accountId) {
return locks.computeIfAbsent(accountId, k -> new ReentrantLock());
}
public void transfer(String from, String to, BigDecimal amount) {
// LOCK ORDERING: always lock the smaller account ID first
String first = from.compareTo(to) < 0 ? from : to;
String second = first.equals(from) ? to : from;
getLock(first).lock();
try {
getLock(second).lock();
try {
BigDecimal fromBal = accounts.getOrDefault(from, BigDecimal.ZERO);
BigDecimal toBal = accounts.getOrDefault(to, BigDecimal.ZERO);
accounts.put(from, fromBal.subtract(amount));
accounts.put(to, toBal.add(amount));
} finally { getLock(second).unlock(); }
} finally { getLock(first).unlock(); }
}
// High concurrency: SAV2001→CUR3001 does NOT block FD4001→SAV5001
// Lock ordering: alphabetical → no circular wait → no deadlock
}
How does lock ordering prevent deadlock?
Deadlock requires four conditions: mutual exclusion, hold and wait, no preemption, and circular wait. Lock ordering breaks circular wait: if all threads always acquire locks in the same global order, no thread can hold a higher-ordered lock while waiting for a lower-ordered one.
What is the performance difference?
| Configuration | Throughput (transfers/sec) |
|---|---|
| Coarse (1 lock, 200 threads) | ~2,000 |
| Fine + ordering (10K locks, 200 threads) | ~45,000 |
| Improvement | 22x |
What are common interview follow-up questions?
- How does ConcurrentHashMap use fine-grained locking? In Java 8+, it uses per-bucket synchronization: CAS for empty buckets, synchronized(firstNode) for non-empty buckets.
- Can lock-free replace fine-grained? For single-variable operations, yes (AtomicInteger). For multi-variable atomic operations (transfer: debit + credit), you still need locking.
Visual Overview
Coarse vs Fine-Grained Locking ══════════════════════════════ Coarse (1 lock): T1: lock(bank) → transfer A→B → unlock T2: lock(bank) → BLOCKED! (waits for T1) T3: lock(bank) → BLOCKED! (waits for T2) All transfers serial! Fine (per-account locks + ordering): T1: lock(A), lock(B) → transfer A→B T2: lock(C), lock(D) → transfer C→D ← PARALLEL! T3: lock(A), lock(C) → waits for A (held by T1) Different accounts: parallel! Same account: serial only when needed.
Key Takeaway
Coarse-grained locking (one lock for everything) is simple and deadlock-free but creates a concurrency bottleneck where all operations serialize through one lock. Fine-grained locking (separate lock per resource) enables high concurrency because operations on different resources run in parallel. The risk of deadlock when acquiring multiple fine-grained locks is eliminated by consistent global lock ordering: always acquire locks in the same deterministic order.
The trade-off is clear: coarse = simple + safe + slow, fine + ordering = complex + fast + safe. For maximum concurrency, lock-free approaches (AtomicInteger, ConcurrentHashMap) avoid locking entirely but require the highest implementation complexity.
At National Bank of India, fund transfers with coarse locking achieved 2,000 transfers per second. Fine-grained per-account locks with alphabetical ordering achieved 45,000 transfers per second, a 22x improvement, with zero deadlocks.
Remember one sentence: fine-grained locking gives 10-20x better concurrency than coarse locking, and consistent lock ordering eliminates the deadlock risk.
---