Synchronization

Thread synchronization in Java — synchronized blocks, locks, volatile, atomic classes, and concurrent collections for thread-safe code.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Synchronization ensures that only one thread accesses a shared resource at a time, preventing race conditions and data corruption. Java provides: synchronized keyword (intrinsic locks), java.util.concurrent.locks (explicit locks), volatile (visibility), atomic classes (lock-free), and concurrent collections. Choose the lightest mechanism that solves your problem.

🔍 What Is It?

Synchronization Mechanisms:

1. synchronized keyword

  • →Intrinsic lock (monitor) on any object
  • →synchronized method: lock is the object (this) or class
  • →synchronized block: lock is the specified object
  • →Reentrant: a thread can re-acquire a lock it already holds

2. ReentrantLock (java.util.concurrent.locks)

  • →Explicit lock with more features than synchronized
  • →tryLock() — non-blocking attempt
  • →lockInterruptibly() — can be interrupted while waiting
  • →Condition variables — fine-grained wait/notify
  • →Fairness option — prevents starvation

3. ReadWriteLock

  • →ReentrantReadWriteLock: multiple readers OR one writer
  • →Ideal for read-heavy, write-rare scenarios

4. volatile

  • →Guarantees visibility (changes visible to all threads)
  • →Does NOT guarantee atomicity (count++ is still a race)
  • →Use for simple flags and status variables

5. Atomic classes (java.util.concurrent.atomic)

  • →AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference
  • →Lock-free using CAS (Compare-And-Swap) hardware instructions
  • →Faster than synchronized for simple operations

6. Concurrent Collections

  • →ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue
  • →Thread-safe collections without external synchronization

🎯 Why Do We Need It?

Without synchronization, concurrent access to shared mutable state causes race conditions — the result depends on the unpredictable timing of thread scheduling. Race conditions cause data corruption, incorrect calculations, and intermittent bugs that are nearly impossible to reproduce in testing.

🏗️ What Happens in Real Projects?

In enterprise code:

  • →ConcurrentHashMap for shared caches (most common)
  • →AtomicLong for thread-safe counters and ID generation
  • →ReentrantReadWriteLock for caches with rare updates
  • →synchronized for simple critical sections
  • →volatile for stop flags in background threads

⚙️ How It Works

CAS (Compare-And-Swap):

Atomic classes use CAS hardware instructions:

  1. Read current value
  2. Compute new value
  3. Atomically: if current value == expected, set to new value; else retry

This is lock-free — no blocking, no deadlock possible.

Memory Model:

Java Memory Model (JMM) defines when changes to memory made by one thread are visible to another. synchronized and volatile both establish happens-before relationships that guarantee visibility.

💻 Example

Synchronization Patterns — Choosing the right synchronization mechanism for each use case.

java
// synchronized — simple, reentrant
class SimpleCache {
    private final Map<String, Object> cache = new HashMap<>();
    
    public synchronized Object get(String key) {
        return cache.get(key);
    }
    
    public synchronized void put(String key, Object value) {
        cache.put(key, value);
    }
    // Problem: readers block each other unnecessarily
}

// ReadWriteLock — better for read-heavy caches
class BetterCache {
    private final Map<String, Object> cache = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
    
    public Object get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();  // ALWAYS unlock in finally!
        }
    }
    
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

// Best for most caches: ConcurrentHashMap (no external locking needed)
class BestCache {
    private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
    
    public Object get(String key) { return cache.get(key); }
    
    public Object getOrLoad(String key, Supplier<Object> loader) {
        return cache.computeIfAbsent(key, k -> loader.get());
    }
}

// AtomicLong for ID generation
class IdGenerator {
    private final AtomicLong counter = new AtomicLong(0);
    
    public long nextId() { return counter.incrementAndGet(); }
}

🤔 Common Confusions

❓ Does synchronized on a method lock the entire class?

No. synchronized on an instance method locks the object instance (this). Different instances can execute synchronized methods simultaneously. Only synchronized static methods lock the Class object. Two threads calling synchronized methods on the same instance block each other; two threads on different instances do not.

❓ Is ConcurrentHashMap fully thread-safe for all operations?

Individual operations (get, put, remove) are thread-safe. Compound operations (check-then-act) are NOT automatically atomic. Example: if (!map.containsKey(k)) map.put(k, v) is a race condition. Use computeIfAbsent() or putIfAbsent() for atomic compound operations.

🚨 Common Production Issues

Lock not released in exception path — if an exception occurs between lock() and unlock(), the lock is never released. Always use try-finally: lock.lock(); try { ... } finally { lock.unlock(); }

Deadlock in production — very hard to detect until it happens. Use jstack to identify BLOCKED threads. Prevention: consistent lock ordering, tryLock with timeout.

Synchronized on wrong object — synchronized(new Object()) creates a new lock each time, providing no synchronization. Always synchronize on the same object.

✅ Best Practices

Use ConcurrentHashMap instead of synchronizing a HashMap.

Use AtomicInteger/AtomicLong instead of synchronized for counters.

Always release locks in finally blocks.

Minimize the scope of synchronized blocks — hold locks as briefly as possible.

Use consistent lock ordering to prevent deadlocks.

Prefer high-level concurrent utilities over synchronized whenever possible.

🏛️ Architect's Note

Senior Architect's Perspective

The best synchronization is no synchronization. Design for immutability (immutable objects are thread-safe by definition), use message passing instead of shared state (actor model, queues), and confine mutable state to a single thread. When you must share mutable state, use the highest-level abstraction available: ConcurrentHashMap over synchronized HashMap, AtomicLong over synchronized counter.

💬 Frequently Asked Questions

What is the Java Memory Model (JMM)?

The JMM defines when writes by one thread are visible to reads by another. Without synchronization, the JVM and CPU can reorder operations and cache values in CPU registers, making changes invisible to other threads. synchronized and volatile create happens-before relationships that guarantee visibility. This is why you cannot safely use a non-volatile boolean flag to stop a thread from another thread.

🎯 Key Takeaways

1synchronized: simple intrinsic lock; ReentrantLock: more features (tryLock, fairness)
2volatile: visibility only, not atomicity; use for simple flags
3AtomicInteger/Long: lock-free CAS operations, faster than synchronized for counters
4ConcurrentHashMap: thread-safe map without external synchronization
5Always release locks in finally blocks
6Prevent deadlocks by acquiring locks in consistent order

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.