Lesson 3 of 728 minModule progress 0%

Module 15: Advanced Concurrency and Async Design

Locks, Conditions, and Atomics

Go beyond intrinsic locks when you need timed locking, explicit signaling, or lock-free counters. You will compare `ReentrantLock`, condition objects, and atomic classes so you can choose the lightest tool that still keeps the code correct.

Author

Java Learner Editorial Team

Reviewer

Technical review by Java Learner

Last reviewed

2026-04-17

Java version

Java 25 LTS

How this lesson was prepared: AI-assisted draft, manually expanded into a full lesson guide, and checked against current official Java, Spring, testing, and delivery documentation.

Learning goals

  • Use `ReentrantLock` safely with `try/finally` and understand when it is worth the extra ceremony
  • Recognize when `Condition` is clearer than ad-hoc wait loops
  • Use atomic classes for focused shared values without pretending they solve every coordination problem

Before you start

  • You are comfortable with classes, methods, exceptions, and collections

Lesson roadmap: Start with the mental model, then follow the design choices, common pitfalls, and the practical workflow you should apply in a real project.

ReentrantLock gives you manual control: It allows timed attempts, explicit unlocks, and advanced coordination features that synchronized does not expose directly.

Conditions provide structured waiting and signaling: They are useful when one thread should pause until another thread changes a specific state.

Atomic classes are a focused alternative for simple shared values: AtomicInteger is great for counters, but it is not a general replacement for all locking.

Design choice: Use the simplest tool that preserves correctness. Atomics for simple counters, locks for coordinated state transitions.

Why move beyond synchronized: The built-in monitor model is often enough, but advanced systems sometimes need timed lock attempts, interruptible waits, or several conditions attached to the same lock.

Condition variables are about state, not just waiting: Good condition-based code defines a specific predicate such as “queue is not empty” or “buffer has space,” then waits until that predicate becomes true again.

Atomics are specialized tools: AtomicInteger, AtomicLong, and friends work well for counters, flags, and compare-and-set updates. They are excellent when one variable is the whole story and less useful when many fields must stay consistent together.

Decision rule: Choose atomics for isolated values, intrinsic locking for simple critical sections, and explicit locks when you need features that intrinsic locking cannot express cleanly.

How to study this module: Run each example more than once, print the current thread name, and change the workload size. Concurrency concepts only become real when you can see scheduling, waiting, and shared-state behavior.

Code review mindset: In concurrent code, correctness comes before throughput. First ask who owns the data, who can mutate it, and what guarantees make each read or write safe.

Production habit: Prefer small, explicit concurrency boundaries. Immutable data, bounded executors, cancellation support, and clear logging are easier to maintain than clever low-level tricks.

Runnable examples

AtomicInteger for a simple counter

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger processed = new AtomicInteger(0);
processed.incrementAndGet();
System.out.println(processed.get());

Expected output

1

`ReentrantLock` must be released in `finally`

import java.util.concurrent.locks.ReentrantLock;

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    System.out.println("critical section");
} finally {
    lock.unlock();
}

Expected output

critical section

Common mistakes

Calling `unlock()` outside a `finally` block

Always pair `lock()` and `unlock()` with `try/finally` so exceptions do not leave the lock held forever.

Using an atomic counter for a bigger object graph that still has inconsistent state elsewhere

If several fields change together, use a coordination approach that protects the whole invariant.

Mini exercise

Write down two scenarios: one where `AtomicInteger` is enough, and one where a queue with a producer and consumer clearly needs a lock plus a condition or a higher-level blocking structure.

Summary

  • Locks give more control than `synchronized`.
  • Conditions support advanced wait/signal coordination.
  • Atomic classes fit simple shared value updates.
  • Explicit locks buy control, but that control comes with more responsibility.
  • Conditions should represent named state changes, not vague “wait until something happens” logic.

Next step

Next, stop creating threads directly and hand work to executor services instead.

Sources used

Advertisement

Lesson check

When is an atomic class often a good fit?

Next lesson →