Synchronization is an invariant design choice before it is a syntax choice. The right question is not “Which primitive is fastest?” It is “Which state transition must other threads observe as one operation, and which component owns it?”
Quick answer
Use synchronized for clear monitor-based mutual exclusion and visibility. Use ReentrantLock when code needs timed or interruptible acquisition, multiple conditions, or explicit lock operations. Use atomic variables for small independent transitions that fit a correct compare-and-set loop. None automatically protects a compound invariant outside its critical section, mutable values stored inside a concurrent collection, or state owned by another process.
Learning objectives
- Select monitor, explicit lock, or atomic operation from the full invariant, cancellation needs, and ownership boundary.
- Review critical sections for safe publication, exception release, blocking calls, lock ordering, and production evidence.
Prerequisites
Read Java Memory Model, Visibility, and Happens-Before first. ConcurrentHashMap Explained shows specialized compound map operations. Follow the full sequence from the Java Concurrency course or topic cluster.
Comparing the primitives
Entering and leaving the same intrinsic monitor provides mutual exclusion and a happens-before edge. Structured synchronized blocks release the monitor on normal or exceptional exit, which makes them a strong default for small critical sections.
ReentrantLock exposes operations such as tryLock(timeout, unit) and lockInterruptibly(). Those can prevent indefinite request waiting, but the owner must unlock in finally. Fair mode changes acquisition policy and can reduce throughput; it is not a blanket correctness upgrade.
Atomic classes implement operations such as increment, exchange, and compare-and-set. CAS may retry under contention. A correct CAS loop must be free of unsafe side effects because its update function can be evaluated more than once. Multiple atomics do not combine themselves into one atomic business transaction.
Java 21 baseline example
This local inventory protects the check and update under one monitor. Returning the remaining value is part of the same transition.
final class LocalInventory {
private int available;
LocalInventory(int available) {
if (available < 0) throw new IllegalArgumentException("available");
this.available = available;
}
synchronized int reserve(int quantity) {
if (quantity <= 0 || quantity > available) return -1;
available -= quantity;
return available;
}
}
The monitor protects only this object in this JVM. A horizontally scaled Spring service must enforce authoritative stock using a database conditional update, optimistic version, or inventory service contract. Holding the monitor across an HTTP call would convert remote latency into local contention and still would not make the remote action transactional.
Lock scope and ordering
Keep critical sections small enough to understand, but large enough to cover the invariant. “Minimize lock scope” is unsafe when it splits the deciding read from the protected write. When several locks are unavoidable, define one global acquisition order. Timed acquisition can bound waiting, but a timeout creates an ambiguous application outcome unless the method states whether any durable change occurred.
Do not block or call unknown code while holding a lock unless the design explicitly accepts the risk. Callbacks can reenter code, remote calls can stall, and logging appenders can introduce surprising dependencies.
Failure scenario: two safe counters, one broken transfer
A service stores debits and credits in separate AtomicLong values. Every individual update is atomic, yet a reader observes a debit before its matching credit and reports an invalid total. Replacing the fields with atomics protected data races but not the pair invariant. One lock around the pair, an immutable state object swapped through one atomic reference, or a database transaction can own the complete transition.
Common misconceptions
synchronizedis not inherently obsolete or always slower than explicit locks.ReentrantLockdoes not release itself unlessunlock()is called infinally.- CAS is not wait-free for every caller and does not make side effects retry-safe.
- Several thread-safe fields do not produce a thread-safe workflow.
- JVM locks do not coordinate replicas, databases, or external services.
Spring boundary
Spring singleton beans are shared across requests. Mutable fields therefore need the same ownership analysis as any other shared object. @Transactional coordinates supported transactional resources; it is not a Java monitor and does not prevent another request from reading memory. @Async changes execution context and does not automatically carry a transaction, SecurityContext, MDC, arbitrary ThreadLocal state, deadline, or cancellation contract across the new boundary.
Production validation
Use JFR lock events and jcmd <pid> Thread.print -l to identify blocked owners and deadlocks. Track acquisition duration and rejected/timed-out operations with bounded cardinality. Connect contention to request latency, CPU, connection waits, and user outcomes. Reproduce the full invariant with barriers and verify exceptional paths release locks. Prefer reversible changes; increasing threads around a contended lock often creates more waiting rather than throughput.
Continue with ExecutorService, Thread Pools, and Work Queues and model the capacity effect in the Java Concurrency Budget Lab.
FAQ
Should I always prefer atomics?
No. They fit small independent transitions. A clear monitor can be safer when several values form one invariant.
Is a fair lock always fair to users?
No. Lock acquisition policy is only one part of request scheduling, deadlines, dependencies, and business priority.
Can a lock make an HTTP call exactly once?
No. A local lock cannot atomically control a remote service. Use stable operation identity, idempotency, reconciliation, and an explicit outcome contract.
Related reading
Use the Java Concurrency learning path, Java Concurrency topic, and Distributed Locks Explained to compare local and multi-process ownership.
Official sources
- Java SE 21 ReentrantLock — accessed August 18, 2026.
- Java SE 21 atomic package — accessed August 18, 2026.
- Java Language Specification 21, synchronization order — accessed August 18, 2026.