The following code sample shows a common way to demonstrate concurrency issues caused by a missing happens-before relationship.
private static /*volatile*/ boolean running = true;
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
while (running) {
// Do nothing
}
}
}.start();
Thread.sleep(1000);
running = false;
}
If running is volatile, the program is guaranteed to terminate after approximately one second. However, if running isn't volatile, the program isn't guaranteed to terminate at all (since there is no happens-before relationship or guarantee for visibility of changes to the variable running in this case) and that's exactly what happens in my tests.
According to JLS 17.4.5 one can also enforce a happens-before relationship by writing to and reading another volatile variable running2, as shown in the following code sample.
private static boolean running = true;
private static volatile boolean running2 = true;
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
while (running2 || running) {
// Do nothing
}
}
}.start();
Thread.sleep(1000);
running = false;
running2 = false;
}
The volatile variable running2 is read in each loop iteration and when it is read as false after approximately one second, it is also guaranteed that the variable running is read as false subsequently, due to the happens-before relationship. Thus the program is guaranteed to terminate after approximately one second and that's exactly what happens in my tests.
However, when I put the read of the variable running2 into an empty if statement inside the while loop, as shown in the following code sample, the program doesn't terminate in my tests.
private static boolean running = true;
private static volatile boolean running2 = true;
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
while (running) {
if (running2) {
// Do nothing
}
}
}
}.start();
Thread.sleep(1000);
running = false;
running2 = false;
}
The idea here is that a volatile read of running2 is like a compiler memory barrier: the compiler has to make asm that re-reads non-volatile variables because the read of running2 might have synchronized-with a release operation in another thread. That would guarantee visibility of new values in non-volatile variables like running.
But my JVM seems not to be doing that. Is this a compiler or JVM bug, or does the JLS allow such optimizations where a volatile read is removed when the value isn't needed? (It's only controlling an empty if body, so the program behaviour doesn't depend on the value being read, only on creating a happens-before relationship.)
I thought the JLS applies to the source code and since running2 is volatile, the effect of reading the variable shouldn't be allowed to be removed due to an optimization. Is this a compiler or JVM bug, or is there a specification, which actually allows such optimizations?



It's neither.
This execution is valid according to the JLS.
The second thread must finish shortly after it reads
running2 == true.But the JLS provides no guarantees about the time it takes for a write in one thread to become visible in another thread.
As a result, your program execution is valid, because it corresponds to the case when the write
running2 = falsetakes a very long time to propagate to another thread.By the way on my java version (OpenJDK 64-Bit Server VM (build 17.0.3+7-suse-1.4-x8664, mixed mode)) the program finishes in about 1 second.
This is also a valid execution — this corresponds to the case when the write
running2 = falsepropagate to the second thread quicker.PS you mentioned "memory barrier".
For a memory barrier there typically exists some max time, after which it is guaranteed to propagate to other threads.
But the JLS doesn't operate in terms of memory barriers, doesn't have to use them, and actually guarantees only this:
PSS If you want to see the real assembly code that the JVM produced for your program you can use +PrintAssembly.