Happens-before does not imply the order for two arbitrary operations. To be more precise, the most important thing that happens-before does is tying up writes and reads in happens-before consistency. Notably, it tells what writes can a read observe: the last write in happens-before order, or any other write not ordered in happens-before (race). Note that two consecutive reads may see different values obtained from different (racy) writes, without violating that requirement.
E.g. JLS 17.4.5 says:
It should be noted that the presence of a happens-before relationship
between two actions does not necessarily imply that they have to take
place in that order in an implementation. If the reordering produces
results consistent with a legal execution, it is not illegal.
Data races are creepy like that: racy reads can return surprising data on each read, and Java memory model captures that. So the more precise answer is that an execution that produces (1, 0) is not violating Java Memory Model constraints (sync order consistency, sync order - program order consistency, happens-before consistency, causality requirements), and therefore allowed.
Implementation-wise: on hardware, both loads can be started and/or arrive to memory subsystem at different times, regardless of their "program order", because they are independent; in compilers, instruction scheduling may also disregard the program order for the independent reads, exposing loads to hardware in "counter-intuitive" order.
If you want reads to be observed in the program order, you need a stronger property. JMM gives that property to synchronization actions (in your example, making a variable volatile
would make that), which ties the actions in the total synchronization order that is consistent with program order. In that case, (1, 0) would be prohibited.
Illustration on a very special jcstress testcase (see the full source for caveats):
private final Holder h1 = new Holder();
private final Holder h2 = h1;
private static class Holder {
int a;
int trap;
}
@Actor
public void actor1() {
h1.a = 1;
}
@Actor
public void actor2(IntResult2 r) {
Holder h1 = this.h1;
Holder h2 = this.h2;
h1.trap = 0;
h2.trap = 0;
r.r1 = h1.a;
r.r2 = h2.a;
}
Even on x86 that does not reorder loads, yields (1, 0), oops:
[OK] o.o.j.t.volatiles.ReadAfterReadTest
(fork: #1, iteration #1, JVM args: [-server])
Observed state Occurrences Expectation Interpretation
[0, 0] 16,736,450 ACCEPTABLE Doing both reads early.
[1, 1] 108,816,262 ACCEPTABLE Doing both reads late.
[0, 1] 3,941 ACCEPTABLE Doing first read early, not surprising.
[1, 0] 84,477 ACCEPTABLE_INTERESTING First read seen racy value early, and the s...
Making Holder.a
volatile would make (1, 0) to go away.