1

So I thought I knew this stuff well enough, until I read something which got me doubting my knowledge on this subject matter. I am almost certain the book is incorrect but would like to ask the community as well.

PS: Have not seen the errata of the book so could well be disclosed as an error.

A simplified example:

public class VolatileMain {

private volatile int a = 0;
private String text = "";

public static void main(String[] args) throws Exception {

    VolatileMain vm = new VolatileMain();

    Thread writer = new Thread() {

        @Override
        public void run() {
            System.out.println("Running thread " + Thread.currentThread().getName());
            vm.text = "hello world";
            vm.a = 5;
        }
    };

    writer.start();
    writer.join();

    System.out.println("Running thread " + Thread.currentThread().getName());
    System.out.println(vm.a);
    System.out.println(vm.text);

   }

}

So given the example is it correct to assume that the write to "text" by Thread writer is guaranteed to be visible by any other thread that reads it?

It seems the author is piggy backing on the volatile semantics of the variable "a" and ensuring that the write to "text" will also be flushed when "a" is flushed, is this a guarantee?

I didn't think it was, but my own quick test (above) to the contrary

Your thoughts.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
JJ Vester
  • 219
  • 1
  • 2
  • 9
  • The example is silly because `join` is a synchronization point anyway, therefore non-volatiles should work just fine in this case. – the8472 Nov 02 '15 at 12:30
  • @the8472 indeed, the thought crossed my mind after submitting the post, however I suspect the intent of the question was well understood by those that answered, thanks for pointing it out though – JJ Vester Nov 02 '15 at 18:11

2 Answers2

4

is it correct to assume that the write to "text" by Thread writer is guaranteed to be visible by any other thread that reads it?

No. But it's guaranteed to be visible by any other thread that reads a before reading text, as your example does:

  • the write of text happens-before the write to a in the writer thread
  • the write of a in writer happens-before the read of a in the main thread
  • the happens-before relation is transitive
  • hence the the write of text happens before the read of a.
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • 1
    Exactly right. It's the reading of `a` that sets up the correct (happens-before) relation between the two threads. You shouldn't think of things like "flushing" when you consider how visibility works in multithreading in Java. There are many more factors in play, and even if it works in a certain way on your hardware, it may work very different in a JVM on other hardware. Just read the Java Memory Model specification (part of the Java Language Specification) https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4 – Erwin Bolwidt Nov 02 '15 at 08:57
  • 1
    @ErwinBolwidt - *You shouldn't think of things like "flushing" when you consider how visibility* . I am not sure if this is right. JVM ensures *Happens-before* by actually sending out / creating Memory barrier instructions. When the processor executes these instructions it (almost always) flushes the local cache into the main memory. in a way flushing is directly related to happens before (memory barriers) – TheLostMind Nov 02 '15 at 09:54
  • 1
    @VinodMadyalkar that entirely an implementation detail. The JVM is cross-platform, and the only thing that it needs to conform to is the JLS and the JVM spec. If you try to think about the underlying hardware, you will run into issues because Java/JVMs will probably exist for another 30 years by which time we'll have hardware architectures that you cannot conceive of now. The Java Memory Model doesn't mention flushing even once. – Erwin Bolwidt Nov 02 '15 at 10:03
  • @ErwinBolwidt - Ya. Different platforms might have different ways of handling memory barriers. I understand that JVM spec makes no mention of how *happens before* should be achieved. But on most modern platforms, flushing cache to main memory is the way mem barriers are handled :) – TheLostMind Nov 02 '15 at 10:14
  • WOW! Thank you all so much for your input, truly valuable insight and knowledge being shared here, I am still trying to wrap my head around the actual semantics and what has been explained in the answers given. My thoughts keep bringing me back to if I were to re-order the mutation instructions of "a" and "text" by writer thread explicitly would that negate the benefit of the subsequent read of "a" by main thread? ie: the value set for "text" could not be guaranteed to be visible by any (main) thread that reads "a" – JJ Vester Nov 02 '15 at 12:31
  • 1
    @VinodMadyalkar weakly ordered memory architectures (e.g. ARM) may order individual writes with some ordering guarantees relative to their data dependencies without ordering all stores, i.e. without flushing the pipeline's write buffer. And I think POWER can even do crazy things ignoring data dependencies. Don't reason about things in x86 semantics, it might fail on other platforms. – the8472 Nov 02 '15 at 12:35
  • Thanks guys, have marked this answer as the most useful in helping fill the gap in my knowledge – JJ Vester Nov 02 '15 at 12:52
4

No, it's not guaranteed, because the "flushing" is not that simple. Even if you actually write non-volatile something into the "main memory", it does not guarantee that consequent reads in other threads will read it from that main memory. Consider the following example:

public class VolatileMain {

    private volatile int a = 0;
    private String text = "";

    public static void main(String[] args) throws Exception {

        VolatileMain vm = new VolatileMain();

        Thread writer = new Thread() {

            @Override
            public void run() {
                // Added sleep here, so waitForText method has chance to JIT-compile
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                System.out.println("Running thread " + Thread.currentThread().getName());
                vm.text = "hello world";
                vm.a = 5;
                System.out.println("Text changed!");
            }
        };

        writer.start();

        waitForText(vm);

        writer.join();

        System.out.println("Running thread " + Thread.currentThread().getName());
        System.out.println(vm.a);
        System.out.println(vm.text);

    }

    // Wait for text change in the spin-loop
    private static void waitForText(VolatileMain vm) {
        int i = 0;
    /*
        @Edit by Soner
        Compiler may do following steps to optimize in lieu.
        String myCache = vm.text;
        -- Assume that here myCache is "" -- so stay forever.
        while (myCache.equals("")) { i++; }
    */
        while (vm.text.equals("")) {
            i++;
        }
        System.out.println("Wait complete: " + i);
    }
}

There are quite good chances that waitForText will never finish, just because JIT-compiler will optimize it and move the reading of the vm.text out of the loop (as it's not volatile, no volatile reads are performed in the loop and text never changes inside the loop) making the loop infinite.

Volatile read/write not only affects the memory commitment, but also changes the JIT-compilation strategy. Add reading of vm.a in the while-loop and the program will work correctly.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334