Back to Synchronization
Q8Question 8 of 8

What is coarse-grained vs fine-grained locking?

Key Concepts
  • Coarse-grained locking uses one lock for all shared state: simple, no deadlock possible, but low concurrency because all threads serialize.
  • Fine-grained locking uses a separate lock per resource (per account, per row): high concurrency because operations on different resources run in parallel.
  • Fine-grained locking risks deadlock when acquiring multiple locks in different orders.
  • Global lock ordering (always acquire locks in deterministic order, e.g., by account ID) breaks circular wait and prevents deadlock.
  • ConcurrentHashMap uses fine-grained per-bucket locking internally.
  • The trade-off is: coarse = simple + safe + slow, fine + ordering = complex + fast + safe.
  • Lock-free approaches (Atomic classes, CAS) provide maximum concurrency with highest complexity.

Definition

Coarse-grained locking uses one lock for everything (simple but bottleneck), while fine-grained locking uses separate locks per resource (high concurrency but deadlock risk), and global lock ordering makes fine-grained safe.
  • 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.

JAVA
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?

JAVA
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate