Reputation: 43
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:
Why fct.isFlag() in the while loop of main thread can't get the latest value without some sleep?
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
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
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
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
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:
volatile
fields. You can try that right now: Make that field volatile
, it'll 'fix' it.synchronized(x)
block is HB vs. entering a synchronized(theSameX)
block in another thread (if that entering that block actually happens afterwards, of course).t.start()
method is HB relative to the first line in the run()
of the thread you started.Some things within the JVM use this stuff.
Tips:
java.util.concurrent
package.Upvotes: 1