Lesson 06 / 11
Java Memory Model
Of eight publication mechanisms, four ask both the writer and the reader to act, one puts the load on the writer alone (a final field), one on the reader alone (join), and two never ask the visibility question at all. The visibility defect of a field without synchronization is not shown by running it; it is modeled as a rule.
Published
In this lesson
- Builds the happens-before relation as a concrete list of rules and writes down which edge each mechanism establishes
- Counts eight publication mechanisms by whether the writer, the reader, or both must act
- Explains, with a modeled setup rather than a run, why a visibility defect cannot be shown by running it
Contents
The previous lesson worked entirely with setups where the same thread both wrote and read a
value; start.await() and t.join() only established order, and the mechanism providing
visibility was already there without ever being named. This lesson names that mechanism. When
one thread writes a value and another thread reads it, the reader seeing the written value is
not automatic — what makes it visible is that a sequencing the language recognizes has been
established between the two actions. This sequencing is called the happens-before relation,
and the question this lesson asks the course is: in each of eight distinct publication
mechanisms, which side has to do something to establish this relation — only the writer,
only the reader, both, or neither?
The “not automatic” part of the question may look like it contradicts the previous lesson’s
examples, where every value was read correctly without any extra effort. The contradiction is
only on the surface: every setup in the previous lesson already used a CountDownLatch or
join, and these two tools — as will be shown below — are themselves happens-before sources in
their own right. Visibility was not automatic there either; the necessary relation was already
established on its own, and this lesson is naming why that establishment was sufficient.
Happens-Before as a Rule
- CC5 — The happens-before relation is not a timing claim; it does not say in what order two actions occurred, it only guarantees that once one has occurred, the results of the other become visible. This relation is never measured anywhere in this lesson; it is only used as a rule.
Among the happens-before rules the language recognizes, the ones used in this lesson are these:
actions within a single thread happen-before each other in program order; releasing a monitor
lock happens-before every later action that acquires the same lock; writing to a volatile
field happens-before every later action that reads that same field; starting a thread
(start()) happens-before every action within that thread; every action within a thread
happens-before another thread successfully returning from a join() on that thread; and the
value of a final field in the constructor happens-before every thread that reaches that object
by safe means after the constructor completes. This list is enough to categorize the eight
mechanisms below.
All of these rules are transitive: if action A happens-before B and B happens-before C, then
A also happens-before C — even if the link in between uses a different mechanism. So in a chain
where a value is written in main, a thread is start()ed, and inside that thread the value is
passed to a second thread with a CountDownLatch, the write at the very start reaches the read
at the very end not in a single jump, but link by link; each link applies its own rule, and
the chain as a whole is not separately proven. Each of the measurements below isolates a
single link of that chain — the direct bond between a writer and a reader.
The Writer’s Cost: final Fields and Safe Publication
- CC6 — The measurement places no extra step in the reader’s code flow; the reader only reads the field. The entire cost is placed on the moment the constructor completes.
// SafePublication.java — the cost of a final field is paid at construction, the reader does nothing extra
public class SafePublication {
static final class Packet {
final int value;
Packet(int value) { this.value = value; }
}
public static void main(String[] args) throws InterruptedException {
Packet box = new Packet(5);
Thread reader = new Thread(() -> {
System.out.println("reader adds nothing, just reads Packet.value: " + box.value);
});
reader.start();
reader.join();
}
}
reader adds nothing, just reads Packet.value: 5
Until the Packet constructor completes, the value field is considered unwritten; the moment
the constructor finishes, this field is frozen. The reader thread reading box.value does
not take a lock or check a volatile field — it applies no extra discipline at all, because the
discipline it would need was already spent on the writer’s side, inside the constructor. In
this lesson, the box reference reaches reader through start(), which is already a channel
that establishes a happens-before relation on its own; the final field’s own guarantee is
broader than that — as long as the constructor does not let this escape early, the field’s
correct value is seen no matter which channel the reference arrives through. This broader
guarantee is not run separately here: proving it would require deliberately publishing the
reference without a happens-before link, and that is exactly the kind of setup the limiting
measurement below avoids.
The guarantee also has a condition: the constructor must not let this escape early. If a
constructor writes this to a static field or registers it with a listener before its own
completion, another thread can see that object while the constructor is still running — and
at that moment the final field may be unassigned or half-assigned. This lesson does not set up
such an escape; it only records that the final field’s guarantee is not unconditional, and
depends on the constructor’s own discipline. SafePublication satisfies this condition:
Packet’s constructor never writes this anywhere, and the constructor finishes right after
value is assigned.
Eight Mechanisms, Four Categories
- CC7 — The six mechanisms below are measured with a bounded loop called
waitUntil: the loop condition is tested up to two hundred million times until it becomes true, and the duration is never printed anywhere. The only thing measured is whether the condition became true or not, and the value read when it does.
// Publication.java — is it enough for the writer and reader to use the same mechanism, across six setups
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
public class Publication {
static volatile int volatileValue = 0;
static volatile boolean volatileReady = false;
static final Object lock = new Object();
static int lockedValue = 0;
static boolean lockedReady = false;
static final AtomicInteger atomicValue = new AtomicInteger(-1);
static int joinValue = 0;
static int latchValue = 0;
static final ThreadLocal<Integer> own = ThreadLocal.withInitial(() -> 0);
static boolean waitUntil(BooleanSupplier condition) {
for (long i = 0; i < 200_000_000L; i++) if (condition.getAsBoolean()) return true;
return false;
}
public static void main(String[] args) throws InterruptedException {
// 1. volatile: the writer writes a volatile field, the reader reads the same volatile field
Thread t1 = new Thread(() -> { volatileValue = 5; volatileReady = true; });
t1.start();
boolean g1 = waitUntil(() -> volatileReady);
System.out.println("volatile : seen=" + g1 + " value=" + volatileValue);
t1.join();
// 2. synchronized: the writer and reader use the same lock
Thread t2 = new Thread(() -> { synchronized (lock) { lockedValue = 5; lockedReady = true; } });
t2.start();
boolean g2 = waitUntil(() -> { synchronized (lock) { return lockedReady; } });
int d2; synchronized (lock) { d2 = lockedValue; }
System.out.println("synchronized : seen=" + g2 + " value=" + d2);
t2.join();
// 3. AtomicInteger: the writer calls set, the reader calls get
Thread t3 = new Thread(() -> atomicValue.set(5));
t3.start();
boolean g3 = waitUntil(() -> atomicValue.get() != -1);
System.out.println("AtomicInteger : seen=" + g3 + " value=" + atomicValue.get());
t3.join();
// 4. join: the body finishes, the reader reads only after join — plain field, nothing else
Thread t4 = new Thread(() -> joinValue = 5);
t4.start();
t4.join();
System.out.println("join : seen=true value=" + joinValue);
// 5. CountDownLatch: the writer calls countDown, the reader calls await — plain field
CountDownLatch latch = new CountDownLatch(1);
Thread t5 = new Thread(() -> { latchValue = 5; latch.countDown(); });
t5.start();
latch.await();
System.out.println("CountDownLatch : seen=true value=" + latchValue);
t5.join();
// 6. ThreadLocal: every thread sees its own copy, never the other thread's at all
Thread t6 = new Thread(() -> own.set(5));
t6.start();
t6.join();
System.out.println("ThreadLocal : caller's own copy=" + own.get()
+ " (the 5 written by the created thread is not in this copy at all)");
}
}
volatile : seen=true value=5 synchronized : seen=true value=5 AtomicInteger : seen=true value=5 join : seen=true value=5 CountDownLatch : seen=true value=5 ThreadLocal : caller's own copy=0 (the 5 written by the created thread is not in this copy at all)
All six lines read the same way, but their justifications differ. For volatile and
synchronized, both the writer and the reader have to touch the same field or the same
lock — if the reader checked a different field or a different lock, no guarantee would remain at
all; these two are written into the both column. AtomicInteger is a third example for
the same reason: the writer calls set, the reader calls get, both through the same object.
CountDownLatch is the same again — the writer must call countDown, the reader must call
await; if either one were missing, the relation would not be established. join carries a
different load: there is nothing extra the writer’s side has to do, it only has to finish
normally; the entire load falls on the reader calling join() — which is why it is in the
reader only column. A final field is the opposite: the entire load is on the constructor
completing, i.e. in the writer only column. ThreadLocal never asks the visibility
question at all — since every thread has its own copy, there is no expectation that the
writer and reader be visible to each other, which is why it goes in the neither column. The
other seven were ways of making a value safe by sharing it; ThreadLocal instead turns not
sharing itself into the tool — the same-named variable living two separate lives in two threads
does not solve the visibility question, it removes it from the question entirely.
| Category | Mechanisms | Count |
|---|---|---|
| writer only | final field |
1 |
| reader only | join |
1 |
| both | volatile, synchronized, AtomicInteger, CountDownLatch |
4 |
| neither | field without synchronization, ThreadLocal |
2 |
Four of the eight mechanisms ask both sides to act, one puts the load solely on the writer, one solely on the reader, and two fall outside the visibility question — one because no rule establishes it, the other because it is isolated enough that no rule needs to.
Placed side by side, join and CountDownLatch look at first glance like they do the same job,
but their places in the table differ — one is in reader only, the other in both. The
distinction comes from this: the happens-before relation join establishes is tied to the
thread’s own natural end — the writer’s side has no extra method it calls, the relation is
established however the body finishes. CountDownLatch is not tied to such a natural end — the
count can drop either before or after the body finishes, through the writer’s side explicitly
calling countDown. This difference makes CountDownLatch more flexible than join (a thread
can signal progress before it finishes), but the price of that flexibility is one extra call
placed on the writer’s side.
Limiting Measurement: Two Modeled Limits
- CC8 — No code is run in this section. Both claims are supported by reference to the rule and by citing earlier courses, not by measurement — because measuring it would either be misleading or impossible by definition.
The first item in the table’s neither row, an ordinary field without synchronization, is
deliberately not run. This is not from laziness, it is a limit: a thread writing an
unsynchronized field and another thread reading it in a loop can, even on the same program on
the same machine, sometimes show the visibility right away and sometimes not — the result
depends on whatever optimization decision the compiler happens to make at that moment, and this
lesson does not measure such a decision, because that is not M08/K04’s own measurement axis.
Running such a loop here and writing “it did not show up” or “it showed up” would be presenting a
one-time observation as a general rule; but the rule is already established above, and no
item covers this field. The correct reading is: if a field does not fall under one of the
seven mechanisms in this list, there is no guarantee at all about that field’s visibility —
neither “visible” nor “invisible” is guaranteed, only not guaranteed.
This means a short-lived trial could always show visibility and mislead the programmer. The
reason the waitUntil loop in Publication can remain a genuine test is that all three fields
it checks (volatileReady, lockedReady, atomicValue) are covered by a rule; if the same loop
checked an unsynchronized boolean field, it would probably still print seen=true — because in
a short-lived program, the compiler may not yet have had the opportunity to reorder the loop.
This apparent success is not proof; it only means that in that particular run, no price was
paid for breaking the rule. The same decision measured in the Just-in-Time Compilation lesson
applies here too: the same source, unchanged, can behave differently depending on a runtime
decision — the difference is that lesson measured the decision’s instruction count, this lesson
only points out that the decision can break observability.
The second limit concerns the volatile field, and it is the one most readily confused with the
visibility guarantee being measured. What volatile gives is visibility and ordering, not
atomicity itself. The read-increment-write trio was established as a concept in the M01/K05
Operating System Concepts course; the same trio can also be split on a volatile field — when
two threads both write counter++ to the same volatile counter, even though both sides do see
the operation, one can perform its own increment before it has read the other’s increment, and
an increment gets lost. This lesson does not run a real race between these two sides,
because the result would be environment-dependent, and how many times the lost increment
happens is not tied anywhere to this lesson’s measurement axis. The claim remains a rule: a
volatile gives visibility for a single read or a single write; it gives nothing for an
operation that combines a read and a write. The actual tool for that is
compare-and-set, on AtomicInteger, measured in the next lesson.
Summary
- Visibility is not automatic; a write becoming visible to a read depends on a happens-before relation the language recognizes having been established between the two.
- Of eight publication mechanisms, four (
volatile,synchronized,AtomicInteger,CountDownLatch) ask both the writer and the reader to act, one (afinalfield) puts the load solely on the writer, one (join) puts the load solely on the reader, and two (a field without synchronization,ThreadLocal) fall outside the visibility question. - The cost of a
finalfield is paid at construction: once the constructor completes, the reader sees the correct value without applying any extra discipline. - The visibility defect of a field without synchronization is not shown by running it; this lesson deliberately models it, because the result would be environment-dependent.
volatilegives visibility and ordering, not atomicity; the read-increment-write trio can still be split on avolatilefield, and this lesson explains that as a rule, not by running it.
Next Step
In this lesson, the synchronized block entered the table as just one row, and it was said
that the reader and the writer need to use the same lock — but the phrase “same lock” was
never questioned itself; lock here was a single static field, and the two threads using the
same object was given from the start. The next lesson removes that assumption and measures who
a monitor lock belongs to: when the same synchronized method is called on two separate
instances, how many separate locks come into play, and how many times is it counted when a
thread re-enters a lock it already holds?
To keep your progress and take notes, Log in
My notes
Log in to take notes.