Welcome back. In the previous two lessons, you separated thread-confined request state from genuinely shared heap state, then examined how temporary and retained heap objects affect garbage collection. Concurrency begins when more than one thread can reach the same mutable object or field.
For a backend service, this can happen in singleton Spring beans, shared caches, request counters, background refresh tasks, executor callbacks, and state passed between threads. This lesson gives you the vocabulary needed to judge whether such shared state is safe: atomicity, visibility, and ordering under the Java Memory Model (JMM). These guarantees are related, but none implies the other.
Three distinct questions about shared state
Consider a singleton metrics component used by many request threads:
final class OrderMetrics {
private int completedOrders = 0;
void recordCompletedOrder() {
completedOrders++;
}
int completedOrders() {
return completedOrders;
}
}
To call this correct under concurrent access, we must ask three different questions.
| Question | Property | What can go wrong without it? |
|---|---|---|
| Does an operation occur as one indivisible action? | Atomicity | Two request threads overwrite each other’s updates. |
| Will one thread observe another thread’s completed write? | Visibility | A worker thread keeps seeing an old flag value. |
| Will related reads and writes be observed in a required sequence? | Ordering | A thread sees “data is ready” but reads stale or incomplete data. |
The Java Memory Model defines the rules that let Java code work consistently across different CPU architectures, JVM implementations, compiler optimizations, and cache hierarchies. It does not mean that every field access must physically go to RAM. Instead, it specifies which results other threads are permitted to observe.
A useful physical intuition is that each CPU core may work with locally cached data, while the JIT compiler and processor may reorder independent instructions to improve performance. But the JMM is the actual programming contract; do not build correctness arguments around a particular cache layout or your current machine.

For a single thread, Java preserves as-if-serial semantics: your program behaves as though that thread ran statements in source order. The difficult part is that another thread may not observe those effects in the order you expect unless your code establishes a synchronization relationship.
Java Memory Model in 10 minutes
Watch “Java Memory Model in 10 minutes” by Defog Tech for a compact visual model of why a program that looks correct in one thread can fail when its fields are shared.
Watch instruction reordering to see why the JVM and CPU are allowed to change execution order when it does not affect single-threaded behavior. Then watch visibility and volatile for the stale-value problem and the role of volatile. Finish with happens before, focusing on the idea that synchronization creates guarantees between threads rather than merely making code “slower but safe.”
Atomicity: can another thread observe an operation halfway through?
An operation is atomic if it appears to occur all at once. No other thread can observe or interfere with a partial execution of that operation.
A plain assignment such as this is atomic:
statusCode = 202;
Likewise, assigning a reference is atomic:
currentCatalog = loadedCatalog;
That does not make the whole business operation thread-safe. Atomic assignment only says a reader sees either the old reference or the new reference, not a half-written reference. It says nothing about whether the reader sees the latest assignment, nor whether the referenced object is being mutated safely.
Why count++ is not atomic
The increment in recordCompletedOrder() looks like one operation in Java source, but it is a compound read-modify-write action:
completedOrders++;
Conceptually, it behaves like:
int observed = completedOrders;
int updated = observed + 1;
completedOrders = updated;
Suppose completedOrders starts at , and two request threads execute this code at nearly the same time:
| Moment | Request thread A | Request thread B |
|---|---|---|
| 1 | Reads | |
| 2 | Reads | |
| 3 | Computes | Computes |
| 4 | Writes | |
| 5 | Writes |
Two orders completed, but the counter ends at , not . This is a lost update.
Making the field volatile does not solve this:
private volatile int completedOrders = 0;
volatile can ensure that each individual read and write is visible, but both threads can still read the same value before either writes its result. The whole read-modify-write transition remains non-atomic.
The same reasoning applies to business rules:
if (availableStock > 0) {
availableStock--;
}
The check and decrement must be treated as one state transition. Otherwise, two threads can both observe one remaining unit and both proceed.
There are two common ways to obtain atomicity:
- Protect the compound operation with a
synchronizedblock or explicit lock. - Use an atomic class such as
AtomicIntegerwhen the state transition is supported by its operations.
For a straightforward counter:
private final AtomicInteger completedOrders = new AtomicInteger();
void recordCompletedOrder() {
completedOrders.incrementAndGet();
}
AtomicInteger.incrementAndGet() performs the update atomically. It is appropriate for a single numeric value, but it is not a general replacement for protecting several related fields or a multi-step business invariant.
Precision for interviews: The JMM guarantees atomic reads and writes for references and most primitive types. Non-volatile
longanddoublereads and writes have a historical specification caveat: they need not be atomic. In normal backend code, usevolatile long,AtomicLong, or synchronization whenever alongis shared and mutable.
Watch “Java Volatile” by Jakob Jenkov for the most important limitation of volatile: it does not make compound updates atomic.
Watch volatile counter race. Focus on the distinction between each thread seeing a current value and the application preserving every increment. The latter requires one thread’s read-modify-write operation to exclude conflicting operations by other threads.
Visibility: does one thread see another thread’s write?
A visibility problem occurs when one thread changes shared state but another thread is allowed to keep reading an older value.
A classic example is a background worker with a stop flag:
final class ReportWorker implements Runnable {
private boolean stopRequested = false;
void requestStop() {
stopRequested = true;
}
@Override
public void run() {
while (!stopRequested) {
processNextBatch();
}
}
private void processNextBatch() {
// process work
}
}
One thread may call requestStop(). Another runs the loop. There is no guarantee that the worker will ever observe the new value of stopRequested. It might repeatedly use a value that was previously read or optimized for the loop.
For a simple flag, volatile is the appropriate tool:
private volatile boolean stopRequested = false;
A write to a volatile field is visible to a later read of that same volatile field by another thread. In this example, the worker will eventually read true and leave the loop.
This is a good use case because the state is simple:
- One thread writes the requested state.
- Other threads read it.
- There is no compound operation such as “read the old value, calculate a new value, and write it back.”
- “Last write wins” is acceptable if more than one thread calls
requestStop().
Publishing related data safely
Visibility becomes more interesting when a flag announces that another value is available.
final class CatalogHolder {
private ProductCatalog catalog;
private volatile boolean initialized;
void initialize(ProductCatalog loadedCatalog) {
catalog = loadedCatalog;
initialized = true;
}
ProductCatalog currentCatalog() {
if (!initialized) {
throw new IllegalStateException("Catalog is not initialized");
}
return catalog;
}
}
The writer first assigns catalog, then writes true to the volatile initialized flag. A reader that sees initialized == true is also guaranteed to see the catalog assignment that occurred before that volatile write.
This pattern is safe only if ProductCatalog is immutable after publication, or if all later mutation is separately synchronized. Publishing a reference safely does not automatically make every future mutation inside that object safe.
In a Spring Boot service, Spring itself safely publishes normal singleton beans during application startup. The risk usually appears when application code adds its own mutable state to singleton services: a refreshable configuration field, a local cache, an in-memory job registry, or a background task’s status.
Ordering: are related actions observed in the required sequence?
Ordering concerns the relationship between multiple actions. It is not merely “the CPU ran lines in a strange order.” The practical question is:
If a consumer observes a signal from a producer, can it safely rely on the producer’s earlier writes?
Consider a producer passing a result to a consumer:
final class ResultSlot {
private ApiResult result;
private boolean published;
void publish(ApiResult newResult) {
result = newResult;
published = true;
}
ApiResult awaitResult() {
while (!published) {
Thread.onSpinWait();
}
return result;
}
}
This has no synchronization. The consumer has no guarantee that observing published == true means it will see the intended result.
Several bad outcomes are permitted by the lack of a cross-thread guarantee:
- The consumer may continue to see
publishedasfalse. - The consumer may see a stale
result. - Optimizations may make the producer’s and consumer’s actions observable in an order that breaks the intended “write result, then signal ready” protocol.
The fix is to make the publication flag volatile:
final class ResultSlot {
private ApiResult result;
private volatile boolean published;
void publish(ApiResult newResult) {
result = newResult;
published = true;
}
ApiResult awaitResult() {
while (!published) {
Thread.onSpinWait();
}
return result;
}
}
The volatile field creates an ordering boundary:
- The producer’s ordinary write to
resultmust remain before its volatile write topublished. - The consumer’s volatile read of
publishedmust occur before its later read ofresult. - When the consumer observes the volatile write, the producer’s earlier write to
resultis visible to it.
The example uses spinning only to isolate the JMM issue. In a real service, a spinning loop wastes CPU and is rarely how you should coordinate work. Executors, futures, blocking queues, and other concurrency utilities usually express the intent more clearly. You will work with such APIs later; for now, focus on the guarantee behind safe communication.
Happens-before: the rule that connects the guarantees
The JMM formalizes visibility and ordering with the happens-before relationship.
If action A happens-before action B, then the effects of A are guaranteed to be visible to B, and B cannot observe A as occurring after it. This is a guarantee of correctly synchronized code, not a claim that one action took place earlier in wall-clock time.
The most useful happens-before rules for everyday backend Java are these:
| Rule | Guarantee |
|---|---|
| Program order | Earlier actions in one thread happen-before later actions in that same thread. |
| Volatile field | A write to a volatile field happens-before a later read of that same field. |
| Monitor lock | Exiting a synchronized block happens-before a later entry to a synchronized block guarded by the same monitor. |
| Thread start | Actions before thread.start() happen-before actions in the started thread. |
| Thread join | All actions in a completed thread happen-before another thread successfully returns from join(). |
| Transitivity | If A happens-before B, and B happens-before C, then A happens-before C. |
The volatile ResultSlot example uses program order, volatile ordering, and transitivity. The assignment to result precedes the volatile write in the producer thread. That volatile write happens-before the consumer’s volatile read. The consumer’s read precedes its use of result.
A synchronized block can establish a stronger guarantee:
final class ResultSlot {
private final Object lock = new Object();
private ApiResult result;
private boolean published;
void publish(ApiResult newResult) {
synchronized (lock) {
result = newResult;
published = true;
}
}
ApiResult currentResult() {
synchronized (lock) {
if (!published) {
throw new IllegalStateException("No result yet");
}
return result;
}
}
}
Using the same lock object matters. The producer releases that monitor when it exits the block; the consumer later acquires the same monitor when it enters its block. This gives visibility and ordering. It also gives mutual exclusion: only one thread can be inside a block guarded by that monitor at a time.
That mutual exclusion is why synchronized can protect a compound state transition while volatile cannot. The next lesson will develop that distinction through a reproducible race condition.
Choosing the right guarantee
Before adding a keyword, identify the operation your code actually needs.
| Situation | Primary need | Appropriate starting point |
|---|---|---|
| One thread updates a shutdown or readiness flag; other threads only read it | Visibility and ordering | volatile |
| One thread publishes an immutable object by writing a volatile reference or readiness flag | Visibility and ordering | volatile, with immutability after publication |
| Multiple threads increment a metric | Atomic read-modify-write | AtomicInteger, LongAdder, or synchronization depending on the metric semantics |
| A stock count, account balance, or state machine has check-then-act logic | Atomic multi-step transition plus visibility | synchronized, Lock, or a carefully designed atomic operation |
| Several fields must always agree with one another | Atomicity, visibility, and ordering across the invariant | One shared lock protecting all relevant access |
| Request-local DTOs, local variables, and method-local collections | None across threads | Keep them thread-confined |
A concise comparison is useful for interview revision:
| Tool | Atomicity | Visibility and ordering | Main limitation |
|---|---|---|---|
| Plain field | Only a single basic read or write | No cross-thread guarantee | Unsafe for unsynchronized shared mutable state |
volatile field | Only each individual read or write | Yes, for communication through that field | Does not make compound operations atomic |
synchronized | Yes, for code guarded by the same monitor | Yes | Requires disciplined use of the same lock |
AtomicInteger and related atomic classes | Yes, for supported single-variable operations | Yes | Does not automatically protect multi-field invariants |
A practical code-review checklist
When you encounter a mutable field in a Spring singleton or other long-lived object, ask:
- Can multiple threads reach this field?
- Is it read-only after construction, or can it change?
- If it changes, does a reader need to see the latest value?
- Is the operation a simple independent assignment, or a read-modify-write transition?
- Does a flag or reference publish related state that must be observed consistently?
- What creates the happens-before relationship:
volatile, a lock, an executor/future API, thread lifecycle, or nothing?
If the answer to the final question is “nothing,” passing tests do not establish correctness. Concurrency bugs often require a particular timing, CPU layout, optimization decision, or production traffic level before becoming visible.
Key takeaways
- Atomicity means an operation is indivisible.
count++, check-then-act logic, and multi-field updates are not atomic by default. - Visibility means one thread can reliably observe another thread’s write. A plain shared field has no such guarantee.
- Ordering ensures that related writes and reads are observed in a safe sequence across threads.
volatileprovides visibility and ordering for communication through one field, but does not make compound operations atomic.synchronizedprovides mutual exclusion, visibility, and ordering when every relevant access uses the same monitor.- Happens-before is the JMM relationship that turns an intended cross-thread protocol into a guarantee.
- Treat mutable fields in singleton services as shared until you can show they are thread-confined or properly synchronized.
For an interview, a strong short answer is:
volatileguarantees visibility and restricts reordering around the volatile field, so it is useful for flags and safe publication. It does not makecount++atomic because incrementing is a read-modify-write sequence. For compound state changes, use synchronization, a lock, or an appropriate atomic class.
Next, you will deliberately reproduce a race condition in shared mutable Java state and learn how to diagnose the resulting lost updates rather than treating them as intermittent test failures.
Can't find a good explanation? Sign up and we'll make it for you
Sign up