Search code examples
javamultithreadingvolatile

Does reading a volatile variable affects the value of other no-volatile variable in thread cache?


Why can the following code stop thread 1? Does reading a volatile variable affects the value of other no-volatile variable in thread cache? When thread 1 read "s", it will also reload the new value of "run"?

public class Vol {

boolean run = true;
volatile int s = 1;
public static void main(String[] args) throws InterruptedException {

    Vol v = new Vol();
    //thread 1
    new Thread(() ->{
        while (v.run) {
            //with this, thread 1 can be shutdown
            int a = v.s;
        }
    }).start();
    //thread 2
    new Thread(() ->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        v.run = false;
        System.out.println("set run false");
    }).start();
}

}


Solution

  • The Java Memory Model (JMM) defines a few guarantees. Beyond these guarantees, VMs are free to do whatever they want. So, usually, the answer to a question of the form: "Is it possible that caches are flushed here?" is yes, because you should be asking more absolutist questions instead: "Is it guaranteed that caches are flushed here?" is a much more interesting question.

    The JMM is based around the notion of 'Happens-Before/Happens-After'. The JMM defines specific scenarios where the JMM will guarantee that code will execute such that what you observe matches the idea that a certain line happened before some other line. For all lines of java code where no HB/HA relationship is established, the JVM may act such that you observe that one ran before the other, or that one ran after another, or even a bizarre mix where parts are observable and other parts are not.

    In other words, consider it like an evil coin: Without HB/HA, the JVM flips a coin for every interaction (a change made by one thread, can this be observed by another? Without HB/HA, the evil coin is flipped). It's evil in that the odds aren't 50/50, it's just to mess with you: During dev and that week-long test it worked out every time, and just when sales is giving that demo to the big customer, it fails.

    There is no easy way to test that coin flips are even happening. Nevertheless, your task is to ensure the evil coin is never flipped. This is difficult, so you should be extremely careful when writing code where multiple threads are reading and writing to the same field.

    volatile access does establish HB/HA relationships. However, it's hard to know in which direction.

    Given this code:

    // shared memory:
    volatile int x = 0;
    /* not volatile */ int y = 0;
    
    // thread A:
    y = 10;
    x = 20;
    
    // thread B:
    if (x == 20 && y != 10) System.out.println("Augh!");
    

    Then you are guaranteed: Augh! can never print. That's because if you read x as 20, the HB/HA relationship guarantees that the line in thread B runs after the second line of A, and everything that A did on that second line or before it is thus guaranteed to be observed. However, you have absolutely no guarantee that x is 20 here. Just, if it is, then the change made to y by thread A is also guaranteed observed, even though y isn't volatile.

    However, in this code:

    // shared state:
    
    /* both non-volatile */ int x = 0, y = 0;
    
    // thread A:
    x = 10;
    y = 20;
    
    // thread B:
    if (y == 20 && x != 10) System.out.println("AUGH!");
    

    Then the JVM is entirely free and evil coin flips are happening! A JVM is free to print AUGH! every time, or never, or sometimes, or only if the moon is full. There is no HB/HA and therefore the VM makes no guarantees about observability and it doesn't have to make sense. It is acceptable for a JVM implementation to allow thread B to observe the change to y but not the change to x even though a simplified but mistaken notion that threads actually run code sequentially would suggest that this would be impossible. Basically, without HB/HA, all bets are off.

    In your snippet, thread A is reading volatile variable s, but that's the only interacting with s available; thread B never touches s.

    Therefore, this code is broken and evil coin flips are occurring! - the JVM is free to work as follows: thread 2 sets run to false today. thread 1 nevertheless runs for 3 more days, and then, seemingly at a completely arbitrary time prompted by nothing you're aware of, all of a sudden it finally observes that run is now false.

    Or, thread 1 stops virtually immediately after thread 2 sets run to false.

    It depends on your OS, the CPU, which song was playing in winamp, the phase of the moon, and whether the butterfly flapped its wings. There is no way to know and no (easy) way to test that you messed up here.