← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›Concurrency & Threading

🧰Synchronization (Types and When to Use What)

Coordinate concurrent access to shared state safely without killing performance.

📌 Key IdeaSynchronization prevents race conditions and visibility bugs when threads share mutable data. Choose primitive by need: synchronized for simple critical sections, Lock variants for advanced control, atomics for counters/CAS, and concurrent collections for shared containers.
🧩

Problem it solves

  • Multiple threads reading/writing the same mutable state can interleave unpredictably and produce wrong results (race conditions).
  • Without proper coordination, one thread may not see the latest value written by another (visibility issue).
  • Synchronization gives a safe way to protect critical sections and preserve invariants in concurrent code.
✨

Why It Matters

  • Without synchronization, race conditions produce incorrect and non-deterministic outcomes.
  • Correctness bugs in concurrency are hard to reproduce and expensive in production.
  • Right primitive selection balances safety, throughput, and maintainability.
✅

When to Use

  • Use synchronized as default for simple shared-state protection.
  • Use ReentrantLock for timeout, interruptible wait, fairness, or conditions.
  • Use atomics for counters and simple CAS transitions.
  • Use concurrent collections instead of manually locking standard collections.
  • Use semaphores to cap parallelism against limited downstream resources.
🚫

When NOT to Use

  • Do not add synchronization when state is immutable or thread-confined.
  • Avoid nested locks with inconsistent ordering (deadlock risk).
  • Avoid over-synchronizing huge code blocks including blocking I/O.
📊

Types of Synchronization and Selection Guide

AspectWhat it givesUse when
synchronized (monitor lock)Mutual exclusion + visibility, simple syntaxSmall/clear critical sections, default choice
ReentrantLockTimed tryLock, interruptible lock, fairness, conditionsNeed lock acquisition control or multiple condition queues
ReadWriteLockMany readers + exclusive writersRead-heavy workloads with occasional writes
StampedLockOptimistic reads + read/write lock modesVery read-heavy hot paths (advanced, careful validation needed)
Atomic* classesLock-free atomic single-variable operationsCounters, flags, simple CAS-based state transitions
volatileVisibility/order only (no compound atomicity)Stop flags, publication of immutable references
Concurrent collectionsThread-safe container operationsShared maps/queues/lists without external coarse locking
Semaphores / Latches / BarriersCoordination and throttlingLimit concurrency, await phases, one-time gates
🔀

Common Confusions Cleared Up

volatile vs lock

volatile only guarantees visibility/ordering for single variable reads/writes. Locks are required for compound invariants and multi-step critical sections.

Atomic* vs synchronized

Atomics are great for single variable non-blocking operations. Once logic spans multiple fields/invariants, lock-based synchronization is safer.

Read-write lock is not always faster

In write-heavy workloads or small critical sections, overhead can outweigh benefits. Measure before adopting.

🧪

Variations / Implementations

1) Intrinsic lock (synchronized)

Best starting point for correctness and readability.

Simple critical sectionjava
class Counter {
  private int value;
  synchronized void inc() { value++; }
  synchronized int get() { return value; }
}

2) Explicit lock (ReentrantLock)

Use when you need timeout/interrupt/fairness/conditions.

Timed lock acquisitionjava
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
  try {
    // critical section
  } finally {
    lock.unlock();
  }
}

3) Read-write synchronization

Improves throughput when reads dominate writes.

ReadWriteLock sketchjava
ReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock();
try { /* read shared state */ } finally { rw.readLock().unlock(); }

4) Lock-free atomics

Use for high-frequency counters and simple atomic updates.

Atomic counterjava
AtomicLong requests = new AtomicLong();
requests.incrementAndGet();

5) Coordination primitives

Use semaphore/latch/barrier for flow control, not mutual exclusion.

Semaphore as concurrency limiterjava
Semaphore permits = new Semaphore(50); // max 50 concurrent tasks
permits.acquire();
try { callExternalApi(); } finally { permits.release(); }
⚠️

Common Mistakes / Anti-Patterns

  • Locking on publicly accessible objects (external interference).
  • Forgetting unlock() in finally with explicit locks.
  • Using volatile for count++ and expecting atomicity.
  • Mixing multiple lock strategies on same state without clear policy.
  • Ignoring contention metrics and lock wait time in production.
🎤

Interview Q&A

🧾

Quick Cheat Sheet

Defaultsynchronized for simple correctness.
Advanced lockReentrantLock for timeout/interrupt/conditions.
Read-heavyReadWriteLock (measure before committing).
Counter/flagAtomic* / volatile depending on atomicity needs.
Flow controlSemaphore/Latch/Barrier for coordination.
🔗

Related Topics