According to the JLS 17 specification section 17.3:
For example, in the following (broken) code fragment, assume that this.done is a non-volatile boolean field:
while (!this.done) Thread.sleep(1000);The compiler is free to read the field this.done just once, and reuse the cached value in each execution of the loop. This would mean that the loop would never terminate, even if another thread changed the value of this.done
I have tried to simulate this following example: 2 threads concurrently access the same boolean variable, the first thread using the shared boolean in while loop, and the second thread update the boolean value.
1.Code without Thread.sleep() inside the first thread:
public boolean done; public void performTest() throws InterruptedException { done = false; new Thread(() -> { System.out.println("Running Thread 1..."); int count = 0; while (!done) { count++; } System.out.println("Exiting thread..."); }).start(); Thread.sleep(100); new Thread(() -> { System.out.println("Thread 2 setting done to true"); done = true; }).start(); }
-> This code would never terminated, because the done
variable not declared as volatile
2.Now change the code to include Thread.sleep() inside while loop as mentioned in the JLS
public boolean done; public void performTest() throws InterruptedException { done = false; new Thread(() -> { System.out.println("Running Thread 1..."); int count = 0; while (!done) { count++; try { Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("Exiting thread..."); }).start(); Thread.sleep(100); new Thread(() -> { System.out.println("Thread 2 setting done to true"); done = true; }).start(); }
-> Now it successfully exists in the first thread.
So i am confused between this example and the JLS mentioned. Not sure what i am missing here.
Note: i also noticed that Venkat’s also mentioned this example in one of his videos, and there is a blog post that explained this behavior, and it looks like there is something related to JIT optimization. What is really concerned me here is that this example is not like what is described in the JLS.
Advertisement
Answer
The reason that the code is broken in your example is because the JVM is free to use a cached version of done
so that your loop would never end. When you have ‘Thread.sleep()’ in there, it is unlikely to happen, but it is still a possiblity. That means, you write some code and test it and it works great. Then you change an environment, or change a JVM and suddenly it is broken.
This is a poor benchmark, but it gives an idea.
public class VolatileTest implements Runnable{ boolean done = false; public void run(){ long count = 0; long start = System.nanoTime(); long end = Integer.MAX_VALUE; while(!done){ count++; if(count == end){ break; } //try{ Thread.sleep(0); } catch (Exception e){ break;} } System.out.println( System.nanoTime() - start + " with " + count + " iterations"); } public static void main(String[] args) throws Exception{ VolatileTest vt = new VolatileTest(); new Thread(vt).start(); Thread.sleep(500); vt.done = true; } }
Now there are 3 cases. 1st as written without any sleep/volatile.
650503733 with 2147483647 iterations
It took 650ms to complete Integer.MAX_VALUE iterations. note sometimes this finishes faster than the 500ms I wait.
2nd case, volatile done
.
499923823 with 1091070867 iterations
Now it never completes before vt.done is set to true.
3rd case. non-volatile with Thread.sleep
499905166 with 3031374 iterations
With the volatile version is 300 times faster than the Thread.sleep version. The non-volatile version is more intermittent in how fast it is but it is the fastest. I suspect due to when the JIT decides to cache done
it gets a speed boost so to speak.
I’m not sure how to verify when it decides to cache the done variable, but I think that why JMH is necessary for these types of micro benchmarks.