Tom Chen
Tom Chen

Reputation: 43

while(true) make the cpu can not read the latest value of shared variable

I have this java code:

class FlagChangeThread implements Runnable {
    private boolean flag = false;
    public boolean isFlag() {return flag;}
    public void run() {
        try {Thread.sleep(300);} catch (Exception e) {}
        // change value of flag to true
        flag=true;
        System.out.println("FlagChangeThread flag="+flag);
    }
}
public class WhileLoop {
    public static void main(String[] args) {
        FlagChangeThread fct = new FlagChangeThread();
        new Thread(fct).start();
        while (true){
            // but fct.isFlag() always get false
            boolean flag = fct.isFlag();
            if(flag){
                System.out.println("WhileLoop flag="+flag);
                break;
            }
        }
    }
}

When i run this code, the whole program only print below message and stuck forever:

FlagChangeThread flag=true

But when i add some sleep times to the while loop of main thread, just like this:

class FlagChangeThread implements Runnable {
    private boolean flag = false;
    public boolean isFlag() {return flag;}
    public void run() {
        try {Thread.sleep(300);} catch (Exception e) {}
        // change value of flag to true
        flag=true;
        System.out.println("FlagChangeThread ="+flag);
    }
}
public class WhileLoop {
    public static void main(String[] args) throws InterruptedException {
        FlagChangeThread fct = new FlagChangeThread();
        new Thread(fct).start();
        while (true){
            Thread.sleep(1);
            boolean flag = fct.isFlag();
            if(flag){
                System.out.println("WhileLoop flag="+flag);
                break;
            }
        }
    }
}

Run it again, the whole program print below message and exit normally:

FlagChangeThread =true
WhileLoop flag=true

I know that declare flag varible to be volatile also fix this problem, becuase when the flag be changed it will be write back to main memory and invalid other cpu's cache line of the flag varible.

But, I have such confusing:

  1. Why fct.isFlag() in the while loop of main thread can't get the latest value without some sleep?

  2. After the flag be changed to true, even thought it is in the thread's working memory right now, but in some point of the future, it will be eventually write back to main memory. Why the main thread can't read this updated value by calling fct.isFlag()? Didn’t it get the flag value from the main memory and copy to main thread's working memory every time it call the fct.isFlag()?

Any body can help me?

Upvotes: 3

Views: 209

Answers (4)

Stephen C
Stephen C

Reputation: 719386

@rzwitserloot's answer covers just about everything. (And what he says about correctness ... is correct.)

The reason why sleep() or println() calls change the behavior is that they have undocumented (serendipitous) effects on the memory cache flushing behavior.

In the case of println, the current implementation of the output stream stack involves calls to internal synchronized methods. This is apparently sufficient to cause your flag's change in value to be visible to the second thread.

In the case of sleep, the call causes the current thread's state to be saved to memory so that execution can switch to a different thread.

But in either case, your modified code is "working" because of undocumented behavior. This behavior could change between different Java versions, across different hardware or OS platforms and so on.

Upvotes: 0

pveentjer
pveentjer

Reputation: 11392

Your code is not working correctly because you are violating the Java Memory model (JMM). The problem with your code is that a happens-before edge is missing between the write of the 'flag' and the read of 'flag' and as a consequence your code is suffering from a data race. When there is a data-race, you can get unexpected behavior. Luckily it is better defined than a data-race with C++ where it can lead to undefined behavior.

The compiler is the typical component that will break this example. It could transform your code into:

if(!flag) return;

while(true){
   ...
}

There is no point in checking flag in the loop if inside the loop the flag isn't changed. This optimization is called loop-invariant code-motion or hoisting. If you would make the flag field volatile, then happens-before edge between the write and the read will exist and the compiler can't apply optimize out the read. Instead it needs to read the flag from 'shared memory' (this include reading it from the coherent CPU cache).

Please do not think that volatile forces flushing to main memory and writing from main memory. Main memory is just a spill bucket for whatever doesn't fit into the CPU cache. Caches on modern CPU's are always coherent. If for every volatile read/write you would need to access main memory, concurrent programs would become very slow. In most cases a volatile read/write can be resolved locally if there is no read/write miss and no cache coherence traffic with other CPU's or main memory is needed. The main 'flushing' that needs to be done to preserve ordering between loads and stores is that loads need to wait for the stores in the store buffer to drain; but this is before the store hits the cache. And even 'flushing' here is an inappropriate term since the store buffer is already draining to the cache as fast as possible.

Also do not believe that volatile prevents using registers in the CPU; modern processors are all load-store architectures which mean that there are separate load/store instructions that load/store from memory into a register and most normal instructions like those executed by the ALUs can only deal with registers and do not have the ability to access memory. Even the X86 which from the outside if a register-memory architecture, after uops conversion becomes a load-store architecture. So registers are always used; the key part is how often registers needs to be synchronized with the cache.

Apart from that, the JMM isn't defined in terms of registers and flushing to main memory, so it isn't a suitable mental model.

Upvotes: 1

Zachary Sang
Zachary Sang

Reputation: 17

Why fct.isFlag() in the while loop of main thread can't get the latest value without some sleep?

I believe that what is happening here is that without the Thread.sleep(1) you have a tight loop (relevant definition). This means means that the main thread is not getting the latest value because it is using the cached value (as you said also fixable by making the flag valuable volatile).

When the Thread.sleep() is added, the tight loop is broken since this moves the thread out of the Runnable state. When a thread moves out of the Runnable state, it is moved off of the CPU. when resuming from Thread.sleep(), the CPU cached value are reloaded from memory, which gives the freshest flag value.

Upvotes: -1

rzwitserloot
rzwitserloot

Reputation: 103648

The reason is The Evil Coin.

The specification that is relevant here is the Java Memory Model (JMM).

The JMM has the following aspect to it:

Any thread is free to make a local cached copy of a variable, or not, and may refer, according to its whims and the phase of the moon if it wants, to either that copy or not.

In other words, the thread flips a coin to decide what to do. It is evil, in that it won't flip heads/tails at a roughly 50/50 split. Assume it flips coins to mess with you: It works great for an hour or so, and then all of a sudden it starts failing when you pick up the work again tomorrow morning, and you have no idea what happened.

Thus, in some of your invocations, that boolean field you're looking at is getting cached copies.

Said differently:

If multiple threads are working with the same field, the behaviour of your application is undefined unless you establish HB/HA.

The reason it works in this bizarre fashion is speed: Any other definition would mean a JVM has to run code a few orders of magnitude more slowly.

The solution is to establish HB/HA: Happens-Before/Happens-After relationships.

HB/HA works like this: If there is an HB/HA relationship between 2 lines of code, then it is impossible to observe the state as it was before the Happens-Before line ran, from the Happens-After line. In other words, if a field has value '5' before the HB line, and value '7' after the 'HB' line, then the HA line cannot possibly observe 5. It can observe 7, or some update that occurred afterwards.

The spec lists a bunch of things that establish HB/HA:

  • Any access to volatile fields. You can try that right now: Make that field volatile, it'll 'fix' it.
  • The exiting of a synchronized(x) block is HB vs. entering a synchronized(theSameX) block in another thread (if that entering that block actually happens afterwards, of course).
  • The t.start() method is HB relative to the first line in the run() of the thread you started.
  • Within one thread, any line of code that is run before any other is HB (that's the trivial case).

Some things within the JVM use this stuff.

Tips:

  • Generally, use stuff from the java.util.concurrent package.
  • Try to avoid interacting with the same field from different threads.
  • Consider databases or message queues or other systems with less finicky rules about inter-thread communications.
  • If writing to fields that other threads are supposed to read, you must consider HB/HA.
  • None of this is a guarantee. You can write broken code that nevertheless passes all tests today, and tomorrow, and next week, and on the production machine, but fails next month when you're giving that important demo to the big customer. Hence, here be dragons: If you mess, you may not know until the cost the bug imposes on you has ballooned out of control. Thus, avoid this stuff unless you really, really, really know what you are doing.

Upvotes: 1

Related Questions