Skip to main content

Command Palette

Search for a command to run...

Java Concurrency — Synchronization & Internal Locks

Updated
9 min read

"Every concurrency bug boils down to one thing: unprotected shared mutable state. Locks are the walls you build around it."

This article covers the complete locking toolkit — from the built-in synchronized keyword to ReentrantLock, ReadWriteLock, and the JVM optimizations that make them fast.


The synchronized Keyword

synchronized is Java's built-in mutual exclusion mechanism. It can be applied to methods or blocks:

Synchronized Methods

public class SafeCounter {
    private int count = 0;

    // Locks on 'this' (the instance)
    public synchronized void increment() {
        count++;
    }

    // Same lock ('this') — mutual exclusion with increment()
    public synchronized int getCount() {
        return count;
    }
}

Synchronized Blocks

public class SafeCounter {
    private int count = 0;
    private final Object lock = new Object();  // Dedicated lock object

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Static Synchronized

public class Registry {
    private static int count = 0;

    // Locks on the Class object: Registry.class
    public static synchronized void increment() {
        count++;
    }
}

Best practice: Use a dedicated private final Object lock = new Object() for locking instead of this. This prevents external code from accidentally (or maliciously) synchronizing on your object.


Object Monitors — How Intrinsic Locks Work

Every Java object has an associated monitor. When a thread enters a synchronized block, it acquires the monitor. Other threads attempting to enter any synchronized block on the same monitor are blocked.

Entry Set vs Wait Set

Set How Thread Gets Here How Thread Leaves
Entry Set Tries to enter synchronized when lock is held Lock becomes available, thread acquires it
Wait Set Calls wait() inside synchronized — releases lock Another thread calls notify() or notifyAll()

Reentrancy

A reentrant lock allows the same thread to acquire it multiple times without deadlocking. Both synchronized and ReentrantLock are reentrant.

public class ReentrantDemo {
    public synchronized void methodA() {
        System.out.println("In methodA");
        methodB();  // Same thread re-acquires the lock — no deadlock!
    }

    public synchronized void methodB() {
        System.out.println("In methodB");
    }
}

If locks were NOT reentrant, calling methodB() from methodA() would deadlock — the thread would wait for a lock it already holds.


volatile — Visibility Without Locking

volatile guarantees visibility and ordering but NOT atomicity:

// Without volatile — Thread B may never see the update
private boolean running = true;

// With volatile — guarantees visibility
private volatile boolean running = true;

When to Use volatile

Use Case volatile synchronized/Lock
Simple flags (stop/resume) Overkill
Compound operations (count++) ❌ Not atomic
Publishing immutable objects
Check-then-act

The Lock Interface — Beyond synchronized

public interface Lock {
    void lock();                             // Blocking acquire
    void lockInterruptibly() throws IE;     // Interruptible acquire
    boolean tryLock();                       // Non-blocking attempt
    boolean tryLock(long time, TimeUnit u)
            throws InterruptedException;     // Timed attempt
    void unlock();                           // Release
    Condition newCondition();                // Condition variable
}

Why Lock Over synchronized?

Feature synchronized Lock
Non-blocking attempt tryLock()
Timed attempt tryLock(time, unit)
Interruptible lockInterruptibly()
Multiple conditions newCondition()
Fair ordering ✅ Constructor flag
Auto-release on exception ❌ Must finally
Can span multiple methods

ReentrantLock — The Workhorse

Basic Pattern

private final ReentrantLock lock = new ReentrantLock();

public void criticalSection() {
    lock.lock();
    try {
        // Protected code
    } finally {
        lock.unlock();  // ALWAYS in finally!
    }
}

tryLock() — Deadlock Avoidance

public boolean transferMoney(Account from, Account to, double amount)
        throws InterruptedException {
    while (true) {
        if (from.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                if (to.getLock().tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        from.debit(amount);
                        to.credit(amount);
                        return true;
                    } finally {
                        to.getLock().unlock();
                    }
                }
            } finally {
                from.getLock().unlock();
            }
        }
        // Both locks not acquired — back off and retry
        Thread.sleep(ThreadLocalRandom.current().nextLong(10, 50));
    }
}

Fair vs Unfair

ReentrantLock unfair = new ReentrantLock();       // Default: unfair (faster)
ReentrantLock fair   = new ReentrantLock(true);   // Fair: FIFO ordering, slower
Aspect Unfair (default) Fair
Throughput Higher Lower (~2x slower)
Starvation Possible Prevented (FIFO)
Use when Performance matters most Every thread must eventually proceed

ReadWriteLock — Concurrent Reads, Exclusive Writes

When reads vastly outnumber writes, using an exclusive lock for reads is wasteful. ReadWriteLock provides:

  • Read lock (shared) — multiple readers simultaneously

  • Write lock (exclusive) — one writer, no readers

Thread-Safe Cache Example

public class Cache<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public V get(K key) {
        rwLock.readLock().lock();
        try {
            return map.get(key);  // Multiple readers can call simultaneously ✅
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void put(K key, V value) {
        rwLock.writeLock().lock();
        try {
            map.put(key, value);  // Exclusive access ✅
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

JVM Lock Optimizations

Modern HotSpot JVM makes synchronized surprisingly fast through four key optimizations:

Optimization Effect When
Biased Locking Zero-cost lock for single-threaded access Uncontended locks (deprecated Java 15+)
Thin Locks CAS on object header — no OS involvement Low contention
Adaptive Spinning CPU spin-wait before parking Recent history suggests lock will be released soon
Lock Elision Removes lock entirely via escape analysis Object never escapes the thread
Lock Coarsening Merges adjacent synchronized blocks Multiple syncs on same lock nearby
// Lock Coarsening — JVM optimizes:
synchronized(lock) { doA(); }
synchronized(lock) { doB(); }
synchronized(lock) { doC(); }

// Into:
synchronized(lock) { doA(); doB(); doC(); }
// Lock Elision — JVM removes lock entirely:
void concat(String a, String b) {
    StringBuffer sb = new StringBuffer();  // sb never escapes method
    sb.append(a);  // synchronized is useless here
    sb.append(b);
    return sb.toString();
}
// JVM: "sb is thread-local, removing all synchronization"

Deadlocks — Detection & Prevention

Deadlock occurs when two or more threads are each waiting for a lock held by the other:

Four Conditions for Deadlock (ALL Required)

  1. Mutual Exclusion — resources are exclusively held

  2. Hold and Wait — thread holds one resource while waiting for another

  3. No Preemption — resources can't be forcibly taken away

  4. Circular Wait — circular chain of threads, each waiting for the next

Prevention Strategies

// Strategy 1: Lock Ordering — always acquire locks in consistent order
// If transferring between account A and B, always lock the lower-ID first
public void transfer(Account a1, Account a2, double amount) {
    Account first  = a1.getId() < a2.getId() ? a1 : a2;
    Account second = a1.getId() < a2.getId() ? a2 : a1;

    synchronized (first) {
        synchronized (second) {
            // Safe — always same order
        }
    }
}

// Strategy 2: tryLock with timeout
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
            try { /* critical section */ }
            finally { lock2.unlock(); }
        }
    } finally { lock1.unlock(); }
}

// Strategy 3: Single lock (coarser granularity)
synchronized (globalLock) {
    // All operations under one lock — no deadlock possible
    // But reduced concurrency
}

Detecting Deadlocks

// Programmatic detection
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = bean.findDeadlockedThreads();
if (deadlockedThreads != null) {
    ThreadInfo[] infos = bean.getThreadInfo(deadlockedThreads, true, true);
    for (ThreadInfo info : infos) {
        System.out.println(info);
    }
}

Choosing the Right Lock


FAQs

Q1: What is the difference between synchronized and ReentrantLock?

A: Both provide mutual exclusion and are reentrant. ReentrantLock adds: tryLock() for non-blocking attempts, lockInterruptibly() for interruptible waits, fairness control, and multiple Condition variables. synchronized has automatic release on exception and is simpler.

Q2: What JVM optimizations make synchronized fast?

A: Biased locking (zero-cost for uncontended), thin locks (CAS on header), adaptive spinning (CPU spin before OS park), lock coarsening (merge adjacent syncs), and lock elision (remove via escape analysis).

Q3: How do you prevent deadlocks?

A: (1) Lock ordering — always acquire locks in a consistent, predefined order. (2) tryLock() with timeout — back off and retry. (3) Use a single coarser lock. (4) Avoid nested locking when possible.

Q4: When would you use ReadWriteLock over synchronized?

A: When reads vastly outnumber writes (e.g., caches, configuration). ReadWriteLock allows multiple concurrent readers while still ensuring exclusive write access. For write-heavy workloads, the overhead of two lock types makes it worse than synchronized.

Q5: What is the difference between volatile and synchronized?

A: volatile guarantees visibility (writes are flushed, reads are fresh) and ordering (prevents reordering) but NOT atomicity. synchronized provides all three: visibility, ordering, AND atomicity. Use volatile for simple flags; use synchronized for compound operations.


Summary

The one-liner: synchronized is simple and JVM-optimized for most cases; ReentrantLock adds tryLock, fairness, and conditions; ReadWriteLock optimizes read-heavy workloads; volatile handles visibility without locking — choose based on your contention profile.