Deep understanding of volatile in Java
Asked Answered
M

4

3

Does Java allows output 1, 0? I've tested it very intensively and I cannot get that output. I get only 1, 1 or 0, 0 or 0, 1.

public class Main {
    private int x;
    private volatile int g;

    // Executed by thread #1
    public void actor1(){
       x = 1;
       g = 1;
    }

    // Executed by thread #2
    public void actor2(){
       put_on_screen_without_sync(g);
       put_on_screen_without_sync(x);
    }
}

Why?

On my eye it is possible to get 1, 0. My reasoning. g is volatile so it causes that memory order will be ensured. So, it looks like:

actor1:

(1) store(x, 1)
(2) store(g, 1)
(3) memory_barrier // on x86

and, I see the following situation: reorder store(g, 1) before store(x,1) (memory_barrier is after (2)). Now, run thread #2. So, g = 1, x = 0. Now, we have expected output. What is incorrect in my reasoning?

Melburn answered 16/7, 2017 at 22:29 Comment(11)
println is a synchronized method - so whatever you're testing is going to benefit from that additional synchronization. You could probably remove the volatile keyword and get the same result (depending on your CPU etc.)...Remonstrance
How could it ever output 1, 0? The fields are initialized to 0, then at some time later assigned 1.Darn
I've edited to avoid a confusion.Melburn
And no, 1,0 is not a legal output (regardless of the unwanted synchronization).Remonstrance
@assylias, why?Melburn
volatile is not specified in terms of memory barriers. If you want a deep understanding of volatile and Java thread synchronization in general, read the Java Language Specification.Orcinol
@Melburn "reorder store(g, 1) before store(x,1)" is not a valid optimisation (it would not comply with the volatile semantics).Remonstrance
@markspace I agree with "Memory visibility is not guaranteed for x until you write g" (hence you could see x=0 in actor2 even if x=1 has been executed). But once g is read in actor2, the JMM guarantees that you will see x=1.Remonstrance
@Melburn what may be confusing you is that the barrier is the volatile store/load, and not a separate operation.Remonstrance
@markspace it isn't legalFresh
OK re-reading this, for the precise case given, it's not legal to see 1,0 However it's really only for this exact sort of case and also making some assumptions about what is being asked here. It could be in other very similar situations that it's legal, even expected, to see something like 1,0.Worldwide
R
2

Any actions before a volatile write happen before (HB) any subsequent volatile read of the same variable. In your case, the write to x happens before the write to g (due to program order).

So there are only three possibilities:

  • actor2 runs first and x and g are 0 - output is 0,0
  • actor1 runs first and x and g are 1 because of the happens before relationship HB - output is 1,1
  • the methods run concurrently and only x=1 is executed (not g=1) and the output could be either 0,1 or 0,0 (no volatile write so no guarantee)
Remonstrance answered 16/7, 2017 at 22:43 Comment(6)
you said "Any actions before a volatile write happen before any subsequent volatile read of the same variable." I don't understand you. After all, x is not the same variable as g.Melburn
x=1 is an action that (due to program order) happens before g=1 which is a volatile write. put_on_screen_without_sync(g); is a volatile read. So any read of x after that statement will benefit from the volatile synchronization and will return 1.Remonstrance
This is a fundamental guarantee of the Java memory model. See chapter 17 of the jls for more details.Remonstrance
@Melburn It means exactly that: any non-volatile writes that happened before the volatile write will be visible after a volatile read.Serration
@assylias, could you look at #45152263?Melburn
it seems to me that you deduce that happens-before is an effect of program order (which is correct) and later (again, correctly) invoke that rule with volatile, as far as I can see you are correct, it's just that this misses the proper JLS explanation, imho. I added it as an addendum.Fresh
H
2

No, this isn't possible. According to the JMM, anything that was visible to thread 1 when it writes to a volatile field becomes visible to thread 2 when it reads that field.

There is another example similar to yours provided here:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
Huda answered 16/7, 2017 at 22:47 Comment(0)
F
1

You will never see 1, 0, but properly explaining this is not going to be easy, spec wise. First let's get some obvious things out of the door. The specification says:

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

This means that on the writing thread side, hb(x, g) and on the reading side hb(g, x). But this is only so, if you would have to reason about each thread individually, as the chapter about Program order says::

Among all the inter-thread actions performed by each thread t...

So if you imagine running each thread at a time, then happens-before would be correct for each of them, individually. But you don't. Your actors (I am sure you use jcstress there) run concurrently. So relying on "program order" for reasoning is not enough (neither it is correct).

You need to somehow synchronize these two actions now - the reading and the writing. And here is how the specification says it can be done:

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

And later says:

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

If you put all of these together now:

          (hb)              (hb)             (hb)
write(x) ------> write(g) -------> read(g) -------> read(x)

This is also called to "transitively" close program order and synchronizes-with order. Since there is hb on every step, seeing 1, 0 (a racy read), is impossible according to the spec.

Fresh answered 30/11, 2020 at 17:13 Comment(0)
S
0

No, and in fact this property of volatile is used in classes like ConcurrentHashMap to implement a lock-free happy path, roughly like this:

volatile int locked = 0;
...
void mutate() {
    if (Unsafe.compareAndSwapInt(locked,0,1)) { 
    /*this isn't exactly how you call this method, but the point stands: 
      if we read 0, we atomically replace it with 1 and continue on the happy 
      path */
       //we are happy
       //so we mutate the structure and then
       locked = 0;           
    } else {
       //contended lock, we aren't happy
    }
}

Since writes before a volatile write can't be reordered after the volatile write, and reads after volatile read can't be reordered before the volatile read, code like this indeed works as a "lockless locking".

Serration answered 16/7, 2017 at 22:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.