skip to content

Compare coarse-grained and fine-grained locking. What are the trade-offs, and how does lock striping help?

level: seniorimportance: must knowfreq 68%

answer

  1. Granularity = state per lock; it's a spectrum
  2. Coarse: simple, no deadlock, but serializes
  3. Fine: concurrent, but deadlock + complexity
  4. Striping: N segments, contention ~1/N
  5. CHM: segments (Java 7) → per-bin + CAS (Java 8+)
  6. Start coarse, measure, then split

basics

~20 s

A coarse-grained lock protects everything with one lock: simple, but all threads queue on it. Fine-grained locking uses many locks for different pieces of data, so unrelated threads don't block each other — but it's more complex and can cause deadlock. Lock striping splits a structure into segments, each with its own lock.

solid answer

~50 s

Granularity is how much state one lock guards. A coarse-grained lock (one lock for the whole object/collection) is simple and easy to reason about — there's no lock-ordering puzzle and no risk of deadlock from multiple locks — but it serializes all access, so it becomes a bottleneck under contention. Fine-grained locking gives each independent piece of state its own lock, so threads touching different data run concurrently, raising throughput; the costs are more memory, more complex code, and the risk of deadlock (you now must acquire multiple locks in a consistent global order) and of subtle bugs when an operation spans several locks. Lock striping is the middle ground: partition the data into N stripes/segments, each with its own lock, and a key's hash picks its stripe — so contention drops by roughly a factor of N without needing a lock per element. ConcurrentHashMap historically used striping (segments); modern versions lock per-bin. Choose granularity by measuring contention, not by default.

code

java · 27 lines
java
// Lock striping: N stripe locks instead of one global lock.
class StripedCounters {
    private final Object[] locks;
    private final long[] counts;

    StripedCounters(int stripes) {
        locks = new Object[stripes];
        counts = new long[stripes];
        for (int i = 0; i < stripes; i++) locks[i] = new Object();
    }

    void increment(String key) {
        int s = Math.floorMod(key.hashCode(), locks.length);
        synchronized (locks[s]) {      // only this stripe is locked
            counts[s]++;               // keys in other stripes run concurrently
        }
    }

    // Whole-structure read must lock EVERY stripe (in fixed order).
    long total() {
        long sum = 0;
        for (int i = 0; i < locks.length; i++) {
            synchronized (locks[i]) { sum += counts[i]; }
        }
        return sum;
    }
}

go deeper

for a junior

Knows one big lock is simpler but slower under load, and that you can use multiple locks for more concurrency.

for a middle

Can articulate the trade-off (simplicity/safety vs concurrency) and give striping as an example, naming ConcurrentHashMap.

for a senior

Reasons precisely about deadlock from multiple locks, the lock-ordering remedy, whole-structure operations needing all stripes, and chooses granularity from measured contention.

for a principal

Weighs striping vs lock-free vs immutability/sharding at architecture scale, knows the CHM segment→per-bin evolution and why, and sets team conventions for lock ordering and measurement-driven decisions.

## What 'granularity' means **Lock granularity** is the *amount of shared state a single lock protects*. - **Coarse-grained** = one lock guards a large amount of state (e.g. one `synchronized` lock for an entire collection, or `Collections.synchronizedMap`, which guards the whole map). - **Fine-grained** = many locks, each guarding a small, independent slice of state (e.g. one lock per bucket, per row, per account). It's a **spectrum**, and the right point depends on how threads actually access the data. ## Coarse-grained: simple but serializing **Pros:** - **Simple to reason about** — there's exactly one lock, so the rule is 'hold it to touch the data.' - **No deadlock from lock ordering** — with a single lock there's no second lock to wait on, so the classic cyclic-wait deadlock can't arise from *this* lock. - **Invariants spanning the whole structure are trivially safe** — you hold the one lock for any multi-part operation. **Cons:** - **Serializes everything.** Two threads touching completely unrelated parts of the structure still block each other. Under load this single lock becomes a **bottleneck** — the scalability ceiling. ## Fine-grained: concurrent but complex **Pros:** - **More concurrency.** Threads operating on different slices proceed in parallel, so throughput scales with the number of independent slices. **Cons:** - **Deadlock risk.** Once an operation needs *two or more* locks (e.g. transfer between two accounts), threads acquiring them in different orders can form a **cyclic wait** and deadlock. The defense is a **global lock-ordering** discipline (always acquire locks in a fixed total order, e.g. by account id) or `tryLock`-with-backoff. - **Complexity and bugs.** Operations that span multiple slices need careful multi-lock handling; whole-structure invariants (like an accurate `size()`) become expensive or approximate. - **More memory and overhead.** Each lock object has a cost; thousands of per-element locks add up. ## Lock striping — the pragmatic middle **Lock striping** partitions the data into a fixed number **N of stripes** (a.k.a. segments/buckets), each protected by its own lock. To operate on a key, you compute `stripe = hash(key) % N` and lock only that stripe. - Independent keys usually fall in different stripes → they don't contend. Expected contention drops by ~**1/N**. - You get most of the concurrency benefit of fine-grained locking **without** a lock per element (N is small and fixed, e.g. 16). - The cost: operations that need a **consistent view of the whole structure** (resize, `size()`, `clear()`) must acquire **all** stripe locks in order — rare but expensive. **`ConcurrentHashMap`** is the textbook example: Java 5–7 used **segment striping** (default 16 segments, so up to 16 writers concurrent); Java 8+ replaced segments with **per-bin (per-bucket) locking** plus CAS for even finer granularity and better scaling. `ConcurrentHashMap.newKeySet()` and Guava's `Striped` expose striping directly. ## How to choose 1. **Start coarse.** It's correct and simple. 2. **Measure** contention (JFR 'Java Monitor Blocked', async-profiler lock mode, thread dumps full of `BLOCKED`). 3. **If a single lock is the proven bottleneck**, move to striping (usually enough) or fine-grained, accepting the lock-ordering discipline. 4. **Consider alternatives first:** a `ConcurrentHashMap`, a lock-free structure, `LongAdder`, or immutability may beat hand-rolled fine-grained locks with less risk. The meta-point: finer locking trades **simplicity and safety** for **concurrency**. Buy that trade only where measurement shows you need it.

  • With fine-grained locking, how do you prevent deadlock when an operation needs two locks?
    Impose a global total order on locks (e.g. acquire by ascending account id or identity hash) and always acquire in that order, or use tryLock with timeout/backoff. Consistent ordering breaks the cyclic-wait condition required for deadlock.
  • Why did ConcurrentHashMap move from segment striping to per-bin locking in Java 8?
    Per-bin (per-bucket) locking plus CAS is even finer-grained than 16 segments, so it scales to more concurrent writers and removes the fixed segment ceiling, while also reducing memory and improving common-case (uncontended) performance.

saying these in an interview costs you the question

  • Jumping to fine-grained locking without measuring contention first
  • Forgetting global lock ordering, then hitting deadlock with multiple locks
  • Claiming fine-grained locking is always faster — it adds overhead and may not help
  • Assuming size()/clear() are cheap on a striped structure — they need all locks

context