Search code examples
javamultithreadingcachingjava-memory-model

Is Thread.yield guaranteed to flush reads/writes to memory?


Lets save I have this code which exhibits stale cache reads by a thread, which prevent it from exiting its while loop.

class MyRunnable implements Runnable {
    boolean keepGoing = true; // volatile fixes visibility
    @Override public void run() {
        while ( keepGoing ) {
            // synchronized (this) { } // fixes visibility
            // Thread.yield(); // fixes visibility
            System.out.println(); // fixes visibility 
        }
    }
}
class Example {
    public static void main(String[] args) throws InterruptedException{
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        Thread.sleep(100);  
        myRunnable.keepGoing = false;
    }
}

I believe the Java Memory Model guarantees that all writes to a volatile variable synchronize with all subsequent reads from any thread, so that solves the problem.

If my understanding is correct the code generated by a synchronized block also flushes out all pending reads and writes, which serves as a kind of "memory barrier" and fixes the issue.

From practice I've seen that inserting yield and println also makes the variable change visible to the thread and it exits correctly. My question is:

Is yield/println/io serving as a memory barrier guaranteed in some way by the JMM or is it a lucky side effect that cannot be guaranteed to work?


Edit: At least some of the assumptions I made in the phrasing of this question were wrong, for example the one concerning synchronized blocks. I encourage readers of the question to read the corrections in the answers posted below.


Solution

  • Nothing in the specification guarantees flushing of any kind. This simply is the wrong mental model, assuming that there has to be something like a main memory that maintains a global state. But an execution environment could have local memory at each CPU without a main memory at all. So CPU 1 sending updated data to CPU 2 would not imply that CPU 3 knows about it.

    In practice, systems have a main memory, but caches may get synchronized without the need to transfer the data to the main memory.

    Further, discussing memory transfers end up in a tunnel vision. Java’s memory model also dictates, which optimizations a JVM may perform and which not. E.g.

    nonVolatileVar = null;
    Thread.sleep(100_000);
    if(nonVolatileVar == null) {
      // do something
    }
    

    Here, the compiler is entitled to remove the condition, and perform the block unconditionally, as the preceding statement (ignoring the sleep) has written null and other thread’s activities are irrelevant for non-volatile variables, regardless of how much time has elapsed.

    So when this optimization has been performed, it doesn’t matter how many threads write a new value to this variable and “flush to memory”. This code won’t notice.

    So let’s consult the specification

    It is important to note that neither Thread.sleep nor Thread.yield have any synchronization semantics. In particular, the compiler does not have to flush writes cached in registers out to shared memory before a call to Thread.sleep or Thread.yield, nor does the compiler have to reload values cached in registers after a call to Thread.sleep or Thread.yield.

    I think, the answer to your question couldn’t be more explicit.

    For completeness

    I believe the Java Memory Model guarantees that all writes to a volatile variable synchronize with all subsequent reads from any thread, so that solves the problem.

    All writes made prior to writing to a volatile variable will become visible to threads subsequently reading the same variable. So in your case, declaring keepGoing as volatile will fix the issue, as both threads consistently use it.

    If my understanding is correct the code generated by a synchronized block also flushes out all pending reads and writes, which serves as a kind of "memory barrier" and fixes the issue.

    A thread leaving a synchronized block establishes a happens-before relationship to a thread entering a synchronized block using the same object. If using a synchronized block in one thread appears to solve the issue despite you’re not using a synchronized block in the other, you’re relying on side effects of a particular implementation which is not guaranteed to continue to work.