JDK-8228899 : While loop does not break on boolean condition
  • Type: Bug
  • Component: hotspot
  • Sub-Component: compiler
  • Affected Version: 12.0.2
  • Priority: P4
  • Status: Closed
  • Resolution: Not an Issue
  • OS: linux
  • CPU: x86_64
  • Submitted: 2019-07-30
  • Updated: 2019-07-31
  • Resolved: 2019-07-31
Related Reports
Relates :  
Description
ADDITIONAL SYSTEM INFORMATION :
OS - Ubuntu 18.04.1 LTS
Tested with both java version - 1.8.0_121
and java version - 12.0.2

A DESCRIPTION OF THE PROBLEM :
If the while body has a small number of instructions, then it won't break the while loop if the boolean condition changed externally.

I found a similar issue - https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8179334
however even when the while body is not empty, this can be reproducible, 
I have checked the bytecodes in this scenario, it seems for me jump instructions are correctly generated, hence I thought maybe some issue with the hotspot VM


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Code I have pasted in the source code section wouldn't exit the while loop. 



EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
While loop should exit
ACTUAL -
While loop keeps on running

---------- BEGIN SOURCE ----------
public class Main {     
     
        public static boolean classLevelVar = true;    
        public static long fact;    
     
        public static void main(String[] args) {    
                breakWhileUsingClassVar();    
        }    
        public static int breakWhileUsingClassVar() {    
                (new Thread(() -> {    
                        try {    
                                Thread.sleep(2000);    
                        } catch (InterruptedException e) {    
                        }    
                        classLevelVar = false;       
     
                        System.out.println("After changing the class level variable - " + classLevelVar);    
                })).start();    
                System.out.println("Before starting the while loop - " + classLevelVar);    
                while (classLevelVar) {    
                        for (int i = 2; i <= 999999999; i++) {    
                                fact = fact * i;    
                        }    
                        for (int i = 2; i <= 999999999; i++) {    
                                fact = fact * i;    
                        }    
                        //for (int i = 2; i <= 999999999; i++) {    
                        //        fact = fact * i;    
                        //}    
                }    
                // Below statement doesn't get hit, making the class level variable volatile solves the issue.    
                return 8;    
        }    
} 
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
If you make the condition boolean "classLevelVar" volatile, then it works, 
and if you uncomment the last for loop, then it works, (Basically if the logic inside while body exceeds a certain threshold, this works properly)
and if you put a SOUT inside the while loop, again it works.

FREQUENCY : always



Comments
Explanation has been already captured here - JDK-8179334 Reiterating again... The program is incorrectly synchronized, as it contains data race. So the behavior is expected. In the example program, classLevelVar is a shared variable, however, it is neither marked as volatile, nor is it accessed in a synchronized block. When compiling the start() method, the C2 compiler therefore assumes that the variable classLevelVar is only accessed locally in the start() method. (That is, the C2 compiler does not consider the possibility that the variable is changed outside the method start()) If the code is changed to access the field in a synchronized block or by marking classLevelVar as volatile, the program behaves as expected, which is already found by the user. Regarding adding extra printf or commenting last for loop there are cases when it is expected that that interpreted and compiled code have a different behavior. Based on the method size C2 can play around with optimization. The reason is that the interpreter and the JIT compilers perform different optimizations. Typically, the interpreter performs very little optimization (if at all), the C1 compiler performs some, and C2 is aggressively optimizing. In the above example, the behavior of the breakWhileUsingClassVar() method is same with the interpreter and C1. C2 generates more aggressively optimized code (for the reasons I pointed out in my previous comment). The description of the Java Memory Model [1] [2] contains an interesting set of examples (incl. possible compiler optimizations) similar to the one provided here. The interpreter will not hoist the read of the non-volatile access out of the loop but the JIT can and C2 will. This is a very old and well known synchronization "puzzler". [1] http://www.cs.umd.edu/~pugh/java/memoryModel/ [2] http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4
31-07-2019