Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A thread is guaranteed to see another thread’s write only when the Java Memory Model provides the required ordering between that write and the read. The key test is whether a documented happens-before path connects them—not whether the write appears earlier in the source code, the threads usually run in a certain order, or a test passed. In practice, that path may come from a monitor, a volatile field, thread lifecycle operations, or a concurrency utility.

What does the Java Memory Model define?

The Java Memory Model (JMM) is the language-level contract for how actions in different threads may interact through shared variables. It defines which executions Java permits; it is not a diagram of processor caches, nor does it promise a particular hardware mechanism such as flushing a cache.

The Java Language Specification (JLS), Java SE 26, defines the relevant actions and their ordering. Its central tool for reasoning about cross-thread communication is happens-before. A program that does not correctly coordinate shared access may have a data race, and its behavior can differ from what an informal reading of source-code order suggests. That does not mean every racy run must show a stale value; it means you lack the stronger guarantees you may be relying on.

What is happens-before?

Happens-before is an ordering relation. It combines ordering within a thread with specified cross-thread synchronization relationships, and it is transitive: if action A happens-before B, and B happens-before C, then A happens-before C.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Program order within one thread

Within a thread, an earlier action in program order happens-before a later action in that thread. This does not, by itself, establish an ordering from one thread to another. Two threads can each have a clear local sequence while still lacking a cross-thread link between them.

Cross-thread synchronization

Synchronization actions can create the link between threads. For example, an unlock of a monitor happens-before a subsequent lock of that same monitor. A write to a volatile field synchronizes with subsequent reads of that same field. Once a cross-thread edge exists, program order and transitivity can carry it to other actions.

When a write happens-before a read, the JMM constrains what that read may observe. In a simple protocol with no intervening write, the read cannot behave as though the ordered write had not occurred. If there are multiple writes, the read must still satisfy the JLS execution-consistency rules; do not assume a read always returns the most recently written value in wall-clock time.

How do synchronized and volatile make changes visible?

Mechanism Cross-thread ordering Mutual exclusion Compound updates Best fit
Ordinary shared field with no coordination No cross-thread happens-before edge is established merely by reading or writing it. No No guarantee for a multi-step operation State confined to one thread, or data protected by another documented protocol
volatile field A volatile write synchronizes with subsequent reads of that same field. No No: a read-modify-write sequence remains multiple actions One-field signaling or a correctly designed publication protocol
synchronized block or method Unlock happens-before a later lock of the same monitor. Yes, for code using that monitor Yes, when the entire invariant-changing operation uses the same monitor Protecting shared state and multi-step invariants
Atomic or concurrent utility Operation-specific memory effects are defined by its API. Depends on the utility; an atomic variable is not a general-purpose lock. Only for the operations the utility documents as atomic A supported atomic state transition or an established coordination protocol

Using synchronized

A synchronized block or method acquires a monitor and releases it when control exits the synchronized region. If one thread releases a monitor and another later acquires that same monitor, the release-to-acquire relationship provides both a visibility/order edge and mutual exclusion for code coordinated through that monitor.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Both threads must use the same monitor for this guarantee. Locking different objects does not create this monitor relationship. To protect an invariant such as “the balance and transaction count change together,” put the relevant reads and writes inside a critical section guarded consistently by one lock.

Using volatile

A volatile write synchronizes with subsequent reads of the same field. This can make volatile useful for a flag or other deliberately designed one-field signaling protocol. For example, if a producer writes ordinary data and then sets a volatile ready flag, and a consumer reads ready as true before reading that data, the volatile synchronization can order the producer’s earlier write before the consumer’s later read. That reasoning assumes the protocol is followed and there is no competing write that changes the data.

Volatile does not exclude other threads from running and does not make a sequence of operations indivisible. In particular, volatile int count; count++; is still a read, an addition, and a write. Two threads can both read the same old value and overwrite one another’s increments. Use a lock or an appropriate atomic operation when the update itself must be atomic.

Which thread and library operations establish ordering?

Synchronization is not limited to explicit locks and volatile fields. The Java SE 26 java.util.concurrent API documents memory-consistency effects for common lifecycle and coordination operations. Use the guarantee of the particular operation rather than assuming that any call into a library automatically publishes all state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Starting a thread: Actions in a thread before calling Thread.start() happen-before actions in the started thread.
  • Joining a thread: Actions in a thread happen-before another thread successfully returns from join() on it.
  • Submitting work: Actions before submitting a task to an ExecutorService happen-before that task begins execution.
  • Getting a result: Actions performed by an asynchronous computation happen-before actions following a successful Future.get().
  • Transferring through concurrent collections: Actions before placing an object into a concurrent collection happen-before another thread’s subsequent access to or removal of that element, as documented for the collection API.
  • Using synchronizers: Locks, semaphores, latches, barriers, and related utilities define memory effects around their documented release/acquire or coordination operations. Check the specific API contract for which operation establishes the edge.

These guarantees often make higher-level utilities easier to reason about than a hand-built signaling protocol. For example, submitting a task and retrieving its result through a future makes the communication boundary explicit in the API.

What is a data race, and why is it risky?

A data race involves conflicting accesses to the same variable that are not ordered by happens-before, with at least one of those accesses being a write. A plain write in one thread and a plain read in another can therefore be racy if no ordering relationship connects them.

A race is not a prediction that one specific stale value will appear. It is a warning that the program cannot rely on the stronger behavior provided by a correctly synchronized protocol. A test that happens to pass does not add a happens-before edge; changing timing, runtime compilation, or hardware may expose behavior the test did not encounter.

Race freedom and atomicity are related but distinct. Proper ordering can make a write visible, yet an operation made up of several steps may still be interrupted by another thread. Conversely, making one update atomic does not automatically protect a larger invariant involving other fields.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Do final fields make an object thread-safe?

No. The JLS gives final fields special initialization semantics: when an object is constructed correctly and does not escape before its constructor completes, another thread that later obtains its reference has guarantees about the initialized final-field values. These rules help with initialization; they are not a general-purpose replacement for synchronization.

A final reference does not make the referenced object’s mutable state immutable or generally thread-safe. If that state changes after construction, coordinate those changes and reads using an appropriate synchronization mechanism. Nor should the final-field rule be stretched into a blanket claim that every object is safely published regardless of how its reference escapes.

How should you choose a coordination mechanism?

  1. Identify the shared state. List the fields read or changed by more than one thread, including fields that form one logical invariant.
  2. State the required guarantee. Decide whether readers need to observe initialization, whether a multi-step update must be indivisible, or whether threads need to wait for a result or event.
  3. Find the documented edge. Point to the matching monitor unlock/lock, volatile write/read, lifecycle operation, or library API guarantee that orders the writer before the reader.
  4. Protect the whole operation. If correctness depends on a check and a later action, or on several fields changing together, ensure the check and action share an appropriate lock or use an abstraction that documents the combined operation.
  5. Test the protocol, not just a schedule. Stress tests can help find bugs, but passing tests do not prove a memory-ordering guarantee. The proof comes from the JLS and the API contract for the operations used.

Which references explain Java memory behavior?

For language guarantees, consult Oracle’s Java Language Specification, Java SE 26, especially its material on synchronization, happens-before order, volatile and final fields, and execution behavior. For practical synchronization and library guarantees, consult Oracle’s java.util.concurrent package documentation for Java SE 26 and the documentation of the specific utility in use.

Java Concurrency in Practice by Brian Goetz and colleagues is a useful conceptual supplement focused on concurrency design and the Java Memory Model. Pearson lists a print paperback, ISBN 9780321349606. The book was published in 2006, so use it alongside current specifications and API documentation rather than as the authority for modern Java features. Oracle’s Java Tutorials further-reading page also lists concurrency books, but notes that the tutorials were written for JDK 8 and may not reflect later improvements.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The JLS and API specifications define behavior; they do not provide a general statistic for how often memory-model bugs occur or how much performance a given synchronization choice saves. Treat numerical claims of that kind as dependent on a specific measured workload, not as a universal property of Java concurrency.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.