Java Memory Model: a JLS statement about sequential consistency seems incorrect

Your error is in bullet point #1: The reads of v1 and v2 are not synchronized-with.

There are happens-before relationships created only by the interactions with vv, so for example in this case, if you added vv to the beginning of your print statement, you would be guaranteed not to see vv=20,v2=4. Since you busy-wait on vv becoming nonzero but then don't interact with it again, the only guarantee is that you will see all of the effects that happened before it became nonzero (the assignments of 1 and 2). You may also see future effects, because you don't have any further happens-befores.

Even if you declare all of the variables as volatile, it is still possible for you to output v1=1,v2=4 because the multithreaded accesses of the variables do not have a defined order, and the global sequence can go like this:

  1. T1: write v1=1
  2. T1: write v2=2
  3. T1: write vv=10 (Thread 2 cannot exit the while loop before here and is guaranteed to see all of these effects.)
  4. T2: read vv=10
  5. T2: read v1=1
  6. T1: write v1=3
  7. T1: write v2=4
  8. T2: read v2=4

After each of these steps, the memory model guarantees that all threads will see the same values of the volatile variables, but you have a data race, and that is because the accesses are not atomic (grouped). In order to assure that you see them in a group, you need to use some other means, such as executing in a synchronized block or putting all of the values into a record class and using volatile or AtomicReference to swap out the entire record.

Formally, the data race as defined by the JLS consists of the operations T1(write v1=3) and T2(read v1) (and a second data race on v2). These are conflicting accesses (because the T1 access is a write), but while both of these events happen after T2(read vv), they are not ordered in relation to each other.

It's actually much easier to prove you are wrong than you think. Actions between two independent threads are "synchronized-with" under very special rules, all of them defined in the proper chapter in the JSL. The accepted answer says that synchronizes-with is not an actual term, but that is wrong. (unless I miss-understood the intent or there is a mistake in it).

Since you have no such special actions to establish the synchronized-with order (SW for short), between Thread1 and Thread2, everything that follows falls like a castle of cards and makes no sense anymore.

You mention volatile, but be careful at the same time what subsequent means in that:

A write to a volatile field happens-before every subsequent read of that field.

It means a read that will observe the write.

If you change your code and establish a synchronizes-with relationship and implicitly thus a happens-before like so:

  v1 = 1;
  v2 = 2;
  vv = 10; 

             if(vv == 10) {
                int r1 = v1;
                int r2 = v2;
                // What are you allowed to see here?

You can start reasoning of what it is possible to be seen inside the if block. You start simple, from here:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

OK, so v1 = 1 happens-before v2 = 2 and happens-before vv = 10. This way we establish hb between actions in the same thread.

We can "sync" different threads via synchronizes-with order, via the proper chapter and the proper rule:

A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread

This way we have established a SW order between two independent threads. This, in turn, allows us to build a HB (happens before) now, because of the proper chapter and yet another proper rule:

If an action x synchronizes-with a following action y, then we also have hb(x, y).

So now you have a chain:

        (HB)          (HB)            (HB)                (HB)
v1 = 1 -----> v2 = 2 -----> vv = 10 ------> if(vv == 10) -----> r1 = v1 ....

So only now, you have proof that that if block will read r1 = 1 and r2 = 2. And because volatile offers sequential consistency (no data races), every thread that will read vv to be 10 will, for sure, also read v1 to be 1 and v2 to be 2.