Skip to content
Advertisement

When not to use volatile,it still can see the changes which issued by other thread

public class VisibleDemo {

    private boolean flag;

    public VisibleDemo setFlag(boolean flag) {
        this.flag = flag;
        return this;
    }

    public static void main(String[] args) throws InterruptedException {
        VisibleDemo t = new VisibleDemo();

        new Thread(()->{
            long l = System.currentTimeMillis();
            while (true) {
                if (System.currentTimeMillis() - l > 600) {
                    break;
                }
            }
            t.setFlag(true);
        }).start();

        new Thread(()->{
            long l = System.currentTimeMillis();
            while (true) {
                if (System.currentTimeMillis() - l > 500) {
                    break;
                }
            }
            while (!t.flag) {
//                if (System.currentTimeMillis() - l > 598) {
//
//                }
            }
            System.out.println("end");
        }).start();
    }
}

if it does not have the following codes, it will not show “end”.

                if (System.currentTimeMillis() - l > 598) {

                }

if it has these codes, it will probably show “end”. Sometimes it does not show.

  1. when is less than 598 or not have these codes, like use 550, it will not show “end”.
  2. when is 598, it will probably show “end”
  3. when is greater than 598, it will show “end” every time

notes:

  • 598 is on my computer, May be your computer is another number.
  • the flag is not with volatile, why can know the newest value.

First: I want to know Why?

Second: I need help,

I want to know the scenarios: when the worker cache of jvm thread will refresh to/from main memory.

OS: windows 10

java: jdk8u231

Advertisement

Answer

Your code is suffering from a data-race and that is why it is behaving unreliably.

The JMM is defined in terms of the happens-before relation. So if you have 2 actions A and B, and A happens-before B, then B should see A and everything before A. It is very important to understand that happens-before doesn’t imply happening-before (so ordering based on physical time) and vice versa.

The ‘flag’ field is accessed concurrently; one thread is reading it while another thread is writing it. In JMM terms this is called conflicting access.

Conflicting accesses are fine as long as it is done using some form of synchronization because the synchronization will induce happens-before edges. But since the ‘flag’ accesses are plain loads/stores, there is no synchronization, and as a consequence, there will not be a happens-before edge to order the load and the store. A conflicting access, that isn’t ordered by a happens-before edge, is called a data-race and that is the problem you are suffering from.

When there is a data-race; funny things can happen but it will not lead to undefined behavior like is possible under C++ (undefined behavior can effectively lead to any possible outcome including crashes and super weird behavior). So load still needs to see a value that is written and can’t see a value coming out of thin air.

If we look at your code:

while (!t.flag) {
   ...
}

Because the flag field isn’t updated within the loop and is just a plain load, the compiler is allowed to optimize this code to:

if(!t.flag){
     while(true){...}
}

This particular optimization is called loop hoisting (or loop invariant code motion).

So this explains why the loop doesn’t need to complete.

Why does it complete when you access the System.currentTimeMillis? Because you got lucky; apparently this prevents the JIT from applying the above optimization. But keep in mind that System.currentTimeMillis doesn’t have any formal synchronization semantics and therefore doesn’t induce happens-before edges.

How to fix your code?

The simplest way to fix your code would be to make ‘flag’ volatile or access both the read/write from a synchronized block. If you want to go really hardcore: use VarHandle get/set opaque. Officially it is still a data-race because opaque doesn’t indice happens-before edges, but it will prevent the compiler to optimize out the load/store. It is a benign data race. The primary advantage is slightly better performance because it doesn’t prevent the reordering of surrounding loads/stores.

I want to know the scenarios: when the worker cache of jvm thread will refresh to/from main memory.

This is a fallacy. Caches on modern CPUs are always coherent; this is taken care of by the cache coherence protocol like MESI. Writing to main memory for every volatile read/write would be extremely slow. For more information see the following excellent post. If you want to know more about cache coherence and memory ordering, please check this excellent book which you can download for free.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement