Source order is not a complete description of what another thread may observe. Compilers, the JVM, processors, and caches may reorder or delay observations while preserving the rules of the Java Memory Model (JMM). Correct concurrent code therefore reasons from defined synchronization actions, not timing guesses.
Quick answer
The JMM separates atomicity, visibility, and ordering. A happens-before relationship guarantees that effects before one action are visible to and ordered before effects after another action. Lock release/acquisition, volatile write/read, thread start/join, and class initialization establish important edges. volatile supplies specific visibility and ordering guarantees, but it does not make a compound read-modify-write such as count++ atomic.
Learning objectives
- Build a happens-before chain for publication, handoff, and shutdown without relying on sleeps or observed timing.
- Distinguish visibility, ordering, single-operation atomicity, and compound business atomicity when selecting a Java primitive.
Prerequisites
Begin with Java Concurrency for Backend Systems. Familiarity with fields, objects, and threads is enough. The Java Concurrency course places this lesson before locks, executors, and virtual threads; the topic cluster lists every related article.
Core model: edges, not freshness guesses
If action A happens-before action B, every effect visible to A is ordered before B. Program order creates edges within one thread. Synchronization order can connect threads. Happens-before is transitive, so several edges form a proof.
Important examples include:
- Unlocking a monitor happens-before a later lock of the same monitor.
- Writing a volatile field happens-before a later read of that same field.
- Actions before
Thread.start()happen-before actions in the started thread. - Actions in a thread happen-before another thread successfully returns from
join(). - Static initialization completes before normal use of that class.
Without a relevant edge, repeated reads are not a reliable publication protocol. “It always changed within a millisecond in testing” is not a JMM guarantee.
Java 21 baseline example
This one-writer publication pattern writes ordinary data before the volatile readiness flag. A reader that observes ready == true also observes the earlier payload write.
final class PublishedConfig {
private String endpoint;
private volatile boolean ready;
void publish(String value) {
endpoint = value;
ready = true;
}
String read() {
if (!ready) throw new IllegalStateException("not ready");
return endpoint;
}
}
This does not support arbitrary concurrent writers, does not make a mutable object graph permanently safe, and does not turn several fields into a transactional update. Prefer immutable values and a single volatile reference when publishing a coherent configuration snapshot.
Atomicity is a separate question
volatile int count does not make count++ atomic. Increment expands conceptually into a read, calculation, and write; two threads can read the same old value and overwrite one another. AtomicInteger.incrementAndGet() can make that one counter transition atomic. A compound invariant such as “debit one account and credit another” still needs a larger lock or durable transaction.
Likewise, a concurrent collection protects only its specified operations. A mutable value retrieved from the map can still be raced, and a rule spanning two maps is not automatically atomic. Continue with ConcurrentHashMap Explained for its documented compound methods.
Failure scenario: shutdown that sometimes hangs
A worker loops on a plain boolean running. Another thread sets it to false, but the worker has no synchronization edge requiring it to observe the write. A load test passes on one machine and hangs on another. Making the flag volatile fixes visibility for that flag, but it still does not unblock an interruptible queue wait. A robust shutdown protocol sets state, interrupts blocking work when appropriate, preserves interruption policy, and waits with a bounded join.
Common misconceptions
volatileis not a general replacement for a lock.- Atomic reads do not make a read-check-write sequence atomic.
- “Effectively immutable” still requires safe publication.
- Thread-safe collections do not protect mutable contents or cross-structure business invariants.
sleep()creates delay, not a happens-before edge between application actions.
Production validation
Express invariants as repeatable concurrent tests with latches or barriers rather than sleeps. Run stress tests many times across optimized builds, but remember that failure absence is not a proof. Review every shared field for ownership and publication. Use JFR and jcmd Thread.print to diagnose live lock or wait behavior, then connect JVM evidence to request outcomes. For durable state, verify database concurrency controls separately; the JMM governs Java memory, not remote processes.
Next compare synchronized, Locks, and Atomic Variables, then learn how ExecutorService and Work Queues introduce explicit task ownership.
FAQ
Does volatile make the referenced object immutable?
No. It governs reads and writes of the reference and their ordering effects. Mutating the referenced object still needs its own safety policy.
Is a data-race-free program automatically correct?
No. It receives strong JMM guarantees, but higher-level ordering and business invariants can still be wrong.
Can tests prove a missing race?
Testing can expose defects and validate designed protocols. The proof still comes from the synchronization and ownership rules that establish the necessary ordering.
Related reading
Return to the Java Concurrency learning path, browse the Java Concurrency topic, and study Optimistic Locking for a durable multi-process concurrency boundary.
Official sources
- Java Language Specification 21, Memory Model — accessed August 18, 2026.
- Java SE 21 AtomicInteger — accessed August 18, 2026.
- Java SE 21 package java.util.concurrent — accessed August 18, 2026.