1

Consider two threads:

A==B==0 (initially)

Thread 1 Thread 2
B=42; if (A==1)
A=1; ...print(B)

To my knowledge if (at least) A is volatile we will only be able to read B==42 at the print. Though if only mark B as volatile we can read B==42 but also B==0.

I want to look at the case where only B is volatile more closely and understand why we can read B==0 based on what these docs say. To do so I started by adding all program order edges and synchronizes with as described in the docs:

enter image description here

The two edges from B=42 to A=1 are simple program order (PO) edges the rest are synchronizes with (SW) edges. According to the docs we have a SW edge when "The write of the default value [...] to each variable synchronizes-with the first action in every thread." (those are the first 4 edges in the picture) and "A write to a volatile variable v [...] synchronizes-with all subsequent reads of v" (the edges from B=42 to print(B)).

Now we can take a look at what happens before edges exists (HB), according to the docs each of these edges is also an HB ordering. Additionally for all hb(x,y) and hb(y,z) we have hb(x,z) (these edges are missing but we will still use those).

Finally, we get from the docs what we can read at print(B) from: "We say that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order [...]:

  • r is not ordered before w (i.e., it is not the case that hb(r, w)), and
  • there is no [...] no write w' to v such that hb(w, w') and hb(w', r) "

Let's see if we can observe a write w (B=0) at a read r (print(B)). We indeed have not hb(r, w). However, we do have a write w' (B=42) intervening with hb(wow') and hb(w',r).

This makes me wonder can we observe B==0 at the print and if yes where is my reasoning or understanding of the docs wrong? I would like a answer that is clearly referring to the docs.

(I have looked at this post however I hope for an explanation referencing the JMM docs more closely, my question also arises from this particular code)

l30c0d35
  • 777
  • 1
  • 8
  • 32

3 Answers3

2

Just to make sure I understand you correctly:

  • your question is:

    This makes me wonder can we observe B==0 at the print and if yes where is my reasoning or understanding of the docs wrong?

  • the java code in question is:
    package my.test;
    
    class MyTest {
    
      static int A; // =0
      static volatile int B; // =0
    
      public static void main(String[] args) throws InterruptedException {
        var t1 = new Thread(() -> {
          B = 42;
          A = 1;
        });
        var t2 = new Thread(() -> {
          if (A == 1) {          // reads A==1 
            System.out.print(B); // reads B==0
          }
        });
    
        t1.start();
        t2.start();
    
        t1.join();
        t2.join();
      }
    }
    

We are interested in the execution where:

  • the read of A in A == 1 reads 1
  • the read of B in System.out.print(B) reads 0

In terms of the JMM actions the execution is this (for brevity only actions on A and B are shown):

Initially:
         write(A=0)
volatile-write(B=0)

Thread1:                Thread2:
volatile-write(B=42)             read(A):1
         write(A=1)     volatile-read(B):0

Here are program-order and synchronizes-with ([po] and [sw] on the diagram) relations between the actions in the execution:

         write(A=0)                         
            ↓[po]                           
volatile-write(B=0)                         
            ││└──────────────────────┐      
            │└───────────────┐       │      
            ↓[sw]            │       ↓[sw]  
volatile-write(B=42)         │     read(A):1
            ↓[po]            ↓[sw]   ↓[po]  
         write(A=1)       volatile-read(B):0

Notes:

  • there is an [sw] edge between initial writes and the first action in every thread because of this rule:

    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.
  • volatile-read(B):0 synchronizes-with volatile-write(B=0) because of this rule:

    A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

    BTW the synchronization order in our case is: volatile-write(B=0) -> volatile-read(B):0 -> volatile-write(B=42).
    According to the synchronization order's definition it's a global order among all synchronization actions and it's consistent with the program order.
    The read volatile-read(B):0 returns 0 because of this rule:

    The execution obeys synchronization-order consistency.

    For all volatile reads r in A, it is not the case that either so(r, W(r)) or that there exists a write w in A such that w.v = r.v and so(W(r), w) and so(w, r).

  • there is no [sw] edge between volatile-write(B=42) and volatile-read(B):0. That's because according to the following rule a volatile write synchronizes-with only those volatile reads of the variable, that come later in the synchronization order:

    A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

  • there is no [sw] edge between write(A=1) and read(A):1 because these are ordinary write and read, but [sw] is only for synchronization actions

And here are happens-before relations (built from [po] and [sw]) between the actions:

         write(A=0)                         
            ↓[hb]                           
volatile-write(B=0)                         
            │└──────────────────────┐       
            ↓[hb]                   ↓[hb]   
volatile-write(B=42)              read(A):1 
            ↓[hb]                   ↓[hb]   
         write(A=1)       volatile-read(B):0

Happens-before consistency according to the JLS:

We say that a read r of a variable v is allowed to observe a write w to v if, in the happens-before partial order of the execution trace:

r is not ordered before w (i.e., it is not the case that hb(r, w)), and

there is no intervening write w' to v (i.e. no write w' to v such that hb(w, w') and hb(w', r)).

Informally, a read r is allowed to see the result of a write w if there is no happens-before ordering to prevent that read.

A set of actions A is happens-before consistent if for all reads r in A, where W(r) is the write action seen by r, it is not the case that either hb(r, W(r)) or that there exists a write w in A such that w.v = r.v and hb(W(r), w) and hb(w, r).

In a happens-before consistent set of actions, each read sees a write that it is allowed to see by the happens-before ordering.

Happens-before consistency isn't violated for read(A):1 because, as you can see in the diagram above, there is no happens-before relations between write(A=1) and read(A):1.

volatile-read(B):0 is also fine (it's explained above).

In fact, I don't see anything in the JMM that is violated in this execution - so IMO the execution is legal according to the JMM.

  • 1
    It’s misleading, at best, to write `volatile-write(B=0)` for the initial write of `B`. As you already cited, the initial writes behave as-if happening at the start of the program already, so there is no ordering relationship between them and there’s nothing before them in program order. – Holger Jun 16 '23 at 10:51
1

I believe your misunderstanding is the write of B=42. Since Thread 2 does not read B until the print(B) statement, there is no happens before relationship for B until after the read of A. therefore the write of B=42 has no affect on the read of A by Thread 2.

So Thread 2 can observe the write of A=1 before the write of B=42.

jtahlborn
  • 52,909
  • 5
  • 76
  • 118
1

The two edges from B=42 to A=1 are simple program order (PO) edges

Making B volatile guarantees that whatever thread 1 did in program order before the B=42 assignment must be visible to thread 2 by the time thread 2 reads 42 from B. But, in your example, thread 1 does nothing before it assigns B=42. So, nothing is what the volatile read guarantees.

Here's something that could happen. The compiler could re-order the two assignments in thread 1, or the hardware could store the values out-of-order. That does not violate "program order" if it doesn't change the outcome of anything that happens within thread 1 itself. So, it could play out like this:

Thread 1      Thread 2
A = 1;
              if (A==1) {                  // succeeds!
                  System.out.println(B);   // prints "0"
B = 42;       }

IDK why those assignments ever would be re-ordered, but I'm pretty sure that the rules allow them to be reordered.

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • this does make sense but I do not really understand where I misunderstand the docs – l30c0d35 Jun 15 '23 at 06:37
  • The CPU can also be a cause of reordering due to OOOe. The 2 stores or 2 loads will probably be performed out of order due to the out-of-order nature of modern processors. It depends very much on the ISA how much of this out-of-order execution can become visible to other CPUs; on the X86 the 2 stores and 2 loads can't become visible out of order, but ARM and RISC-V have a much more relaxed memory model. – pveentjer Jun 16 '23 at 03:45
  • 1
    “by the time thread 2 reads `B`” is a phrase that leads to a widespread misunderstanding. It must be “by the time thread 2 reads `42` from `B` (if it ever reads `42` from `B`)”`. – Holger Jun 16 '23 at 10:33
  • @Holger, I have implemented your suggestion. Thank you. – Solomon Slow Jun 16 '23 at 12:09
  • 1
    @pveentjer, Ditto. Actually, when I wrote my answer, I made a deliberate decision not to complicate the answer by mentioning the memory system. I thought that just saying the compiler could do it would be enough. Hopefully, the change I made above, which just says "the hardware could re-order the stores" will be a satisfactory middle path. – Solomon Slow Jun 16 '23 at 12:14