Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why non volatile variable is updated on CPU shared cache?

flag variable is not volatile so I am expecting to see an infinite loop at Thread 1. But I don't get why Thread1 can see Thread2 updates on flag variable.

Why non volatile variable is updated on CPU shared cache? Is there a difference between volatile and non volatile flag variable here?

static boolean flag = true;

public static void main(String[] args) {

    new Thread(() -> {
        while(flag){
            System.out.println("Running Thread1");
        }
    }).start();

    new Thread(() -> {
        flag = false;
        System.out.println("flag variable is set to False");
    }).start();

}
like image 314
hellzone Avatar asked Dec 30 '22 21:12

hellzone


2 Answers

There are zero guarantees that such a simple program will show a perceivable result. I mean, it's not even guaranteed which thread will start first, at least.

But in general, the visibility effects are only guaranteed by the java language specification, that carefully builds a so-called "happens-before relationship". This is the only guarantee that you have, and that exactly says:

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

without volatile, the safe-net is gone. And you might say - "but I can't reproduce". The answer to that would be:

  • ... on this run

  • ... on this platform

  • ... with this compiler

  • ... on this CPU

and so on.


The fact that you add a System.out.println in there (which internally will have a synchronized part), only aggravates things; in the sense that it takes away more chances to have that one thread run forever.


It took me a while, but I think I can come up with an example on how to prove that this can break. For that, you need a proper tool: designed for these kind of things

@JCStressTest
@State
@Outcome(id = "0", expect = Expect.ACCEPTABLE)
@Outcome(id = "3", expect = Expect.ACCEPTABLE_INTERESTING, desc = "racy read!!!")
@Outcome(id = "4", expect = Expect.ACCEPTABLE, desc = "reader thread sees everything that writer did")
public class NoVolatile {

    private int y = 1;
    private int x = 1;

    @Actor
    public void writerThread() {
        y = 2;
        x = 2;
    }

    @Actor
    public void readerThread(I_Result result) {
        if(x == 2) {
            int local = y;
            result.r1 = x + local;
        }
    }
}

You do not need to understand the code (though this would help), but overall it builds two "actors" or two threads that change two independent values : x and y. The interesting part is this:

if(x == 2) {
     int local = y;
     result.r1 = x + local;
}

if x == 2, we enter that if branch and result.r1 should always 4, right? What if result.r1 is 3, what does this mean?

This would mean that x == 2 for sure (otherwise there would be no write to r1 at all and as such result.r1 would be zero) and it would mean that y == 1.

That would mean that ThreadA (or writerThread) has performed a write (we know for sure that x == 2 and as such y should also be 2), but ThreadB (readerThread) did not observe that y is 2; it has still seen y as being 1.

And these are the cases defined by that @Outcome(....), obviously the one I care about is that 3. If I run this (up to you to figure out how), I will see that ACCEPTABLE_INTERESTING case is indeed present in the output.

If I make a single change:

 private volatile int x = 1;

by adding volatile, I start to follow the JLS specification. Specifically 3 points from that link:

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

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

If hb(x, y) and hb(y, z), then hb(x, z).

This would mean that if I see that x == 2, I must also see that y == 2 (unlike without volatile). If I run now the example, 3 is not going to be part of the result.


This should prove that a non-volatile read can be racy and as such lost, while a volatile one - can't be lost.

like image 191
Eugene Avatar answered Jan 08 '23 04:01

Eugene


On the X86, caches are always coherent. So if CPU1 executes a store on address A and CPU2 has the cache line containing A, the cache line is invalidated on CPU2 before the store can commit to the the L1D on CPU1. So if CPU2 wants to load A after the cache line has been invalidated, it will run into a coherence miss and first needs to get the cache line in e.g. shared or exclusive state before it can read A. And as consequence it will see the newest value of A.

So volatile loads and stores have no influence on caches being coherent. On the X86 it will not happen that on CPU2 the old value is loaded for A, after CPU1 committed A to the L1D.

The primary purpose of volatile is to prevent reordering with respect to other loads and stores to other addresses. On the X86 almost all reorderings are prohibited; only older stores can be reordered with newer loads to a different address due to store buffering. The most sensible way to prevent this is by adding a [StoreLoad] barrier after the write.

For more info see: https://shipilev.net/blog/2014/on-the-fence-with-dependencies/

On the JVM this is typically implemented using a 'lock addl %(rsp),0'; meaning that 0 is added to the stack pointer. But an MFENCE would be equally valid. What happens on a hardware level is that the execution of loads is stopped till the store buffer has been drained; hence older stores needs to become globally visible (store their content in the L1D) before newer loads can become globally visible (load their content from the L1D) and as a consequence the reordering between older stores and newer loads is prevented.

PS: What Eugene said above is completely valid. And it is best to start with the Java Memory Model which is an abstraction from any hardware (so no caches). Apart from CPU memory barriers, there are also compiler barriers; so my story above only provides a high level overview what happens on the hardware. I find it very insightful to have some clue about what happens on the hardware level.

like image 24
pveentjer Avatar answered Jan 08 '23 05:01

pveentjer