This question came to me after reading this answer.
Code example:
class Obj1 {
int f1 = 0;
}
volatile Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
-------------------------------------------------
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
| v2 = v1; |
| | var r1 = v2.f1;
Is (r1 == 0) possible?
Here object o
:
Thread 1
to Thread 2
via the volatile
field v1
Thread 2
to Thread 3
via v2
The question is: Can Thread 3
see o
as partially constructed (i.e. o.f1 == 0
)?
Tom Hawtin - tackline says it can: Thread 3
can see o
as partially constructed, because there is no happens-before relation between o.f1 = 1
in Thread 1
and r1 = v2.f1
in Thread 3
due to unsafe publication.
To be fair, this surprised me: until that moment I thought the 1st safe publication is enough.
As I understand, effectively immutable objects (described in such popular books as Effective Java and Java Concurrency in Practice) are also affected by that problem.
The Tom's explanation seems perfectly valid to me according to happens-before consistency in the JMM.
But there is also the causality part in the JMM, which adds constraints on top of happens-before. So, maybe, the causality part somehow guarantees that the 1st safe publication is enough.
(I cannot say that I fully understand the causality part, but I think I would understand example with commit sets and executions).
So I have 2 related questions:
Thread 3
to see o
as partially constructed?Thread 3
is allowed or prohibited to see o
as partially constructed?Answer: Causality part of the JMM allows Thread 3
to see o
as partially constructed.
I finally managed apply 17.4.8. Executions and Causality Requirements (aka the causality part of the JMM) to this example.
So this is our Java program:
class Obj1 {
int f1;
}
volatile Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
--------------------|----------|-----------------
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
| v2 = v1; |
| | var r1 = v2.f1;
And we want to find out if the result (r1 == 0)
is allowed.
Turns out, to prove that (r1 == 0)
is allowed, we need to find a well-formed execution, which gives that result and can be validated with the algorithm given in 17.4.8. Executions and Causality Requirements.
First let's rewrite our Java program in terms of variables and actions as defined in the algorithm.
Let's also show the values for our read and write actions to get the execution E
we want to validate:
Initially: W[v1]=null, W[v2]=null, W[o.f1]=0
Thread 1 | Thread 2 | Thread 3
----------|----------|-----------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
Notes:
o
represents the instance created by new Obj1();
in the java codeW
and R
represent normal writes and reads; Wv
and Rv
represent volatile writes and reads=
W[o.f1]=0
is in the initial actions because according to the JLS:
The write of the default value (zero, false, or null) to each variable synchronizes-with the first action in every thread.
Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.
Here is a more compact form of E
:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
Validation of E
According to 17.4.8. Executions and Causality Requirements:
A well-formed execution E = < P, A, po, so, W, V, sw, hb > is validated by committing actions from A. If all of the actions in A can be committed, then the execution satisfies the causality requirements of the Java programming language memory model.
So we need to build step-by-step the set of committed actions (we get a sequence C₀,C₁,...
, where Cₖ
is the set of committed actions on the k-th iteration, and Cₖ ⊆ Cₖ₊₁
) until we commit all actions A
of our execution E
.
Also the JLS section contains 9 rules which define when an action can me committed.
Step 0: the algorithm always starts with an empty set.
C₀ = ∅
Step 1: we commit only writes.
The reason is that according to rule 7, a committed a read in Сₖ
must return a write from Сₖ₋₁
, but we have empty C₀
.
E₁:
W[v1]=null, W[v2]=null, W[o.f1]=0
----------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
C₁ = { W[v1]=null, W[v2]=null, W[o.f1]=0, W[o.f1]=1, Wv[v1]=o }
Step 2: now we can commit the read and the write of o
in Thread 2.
Since v1
is volatile, Wv[v1]=o
happens-before Rv[v1]
, and the read returns o
.
E₂:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
C₂ = C₁∪{ Rv[v1]=o, W[v2]=o }
Step 3: now the we have W[v2]=o
committed, we can commit the read R[v2]
in Thread 3.
According to rule 6, a currently committed read can only return a happens-before write (the value can be changed once to a racy write on the next step).
R[v2]
and W[v2]=o
are not ordered with happens-before, so R[v2]
reads null
.
E₃:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=null
C₃ = C₂∪{ R[v2]=null }
Step 4: now R[v2]
can read W[v2]=o
through a data race, and it makes R[o.f1]
possible.
R[o.f1]
reads the default value 0
, and the algorithm finishes because all the actions of our execution are committed.
E = E₄:
W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 | |
Wv[v1]=o | |
| Rv[v1]=o |
| W[v2]=o |
| | R[v2]=o
| | R[o.f1]=0
A = C₄ = C₂∪{ R[v2]=o, R[o.f1]=0 }
As a result, we validated an execution which produces (r1 == 0)
, therefore, this result is valid.
Also, it worth noting, that this causality validation algorithm adds almost no additional restrictions to happens-before.
Jeremy Manson (one of the JMM authors) explains that the algorithm exists to prevent a rather bizarre behavior — so called "causality loops" when there is a circular chain of actions which causes each other (i.e. when an action causes itself).
In every other case except for these causality loops we use happens-before like in the Tom's comment.