Java Concurrency & Async Programming · Lesson 3

synchronized, Locks, and Atomic Variables in Java

Compare Java monitors, ReentrantLock, CAS, atomic variables, lock scope, fairness, and compound invariants through backend examples.

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

  • synchronized is not inherently obsolete or always slower than explicit locks.
  • ReentrantLock does not release itself unless unlock() is called in finally.
  • 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.

Use the Java Concurrency learning path, Java Concurrency topic, and Distributed Locks Explained to compare local and multi-process ownership.

Official sources

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. Two balances must change as one local in-memory transfer; which synchronization scope is required?

2. When does ReentrantLock add a capability beyond a simple synchronized block?