javamultithreadingcachingparallel-processingvolatile

Threads does not work without volatile and reads the value from RAM instead of caching


Volatile is supposed to make the Threads read the values from RAM disabling thread cache, and without volatile caching will be enabled making a thread unaware of the variable change made by another thread but this does not work for the below code.

Why does this happen and code works the same with and without volatile keyword there?

public  class Racing{
    
     private  boolean won = false;  //without volatile keyword
      
     public void race() throws InterruptedException{
        Thread one = new Thread(()->{
            System.out.println("Player-1 is racing...");
            while(!won){
              won=true;
            }
            System.out.println("Player-1 has won...");
        });
        
        Thread two=new Thread(()->{
           System.out.println("Player-2 is racing...");
            while(!won){
               System.out.println("Player-2 Still Racing...");
           } 
        });
        
        one.start();
        //Thread.sleep(2000);
        two.start();
      }
      
      public static void main(String k[]) {
        Racing racing=new Racing();
        try{
             racing.race();
        }
        catch(InterruptedException ie){}
      }
  

Why does this behave the same with and without volatile ?


Solution

  • Volatile is supposed to make the threads read the values from RAM disabling thread cache

    No, this is not accurate. It depends on the architecture where the code is running. The Java language standard itself does not state anything about how volatile should or should not be implemented.

    From Myths Programmers Believe about CPU Caches:

    As a computer engineer who has spent half a decade working with caches at Intel and Sun, I’ve learnt a thing or two about cache-coherency. (...) For another, if volatile variables were truly written/read from main-memory > every single time, they would be horrendously slow – main-memory references are > 200x slower than L1 cache references. In reality, volatile-reads (in Java) can > often be just as cheap as a L1 cache reference, putting to rest the notion that volatile forces reads/writes all the way to main memory. If you’ve been avoiding the use of volatiles because of performance concerns, you might have been a victim of the above misconceptions.

    Unfortunately, there still are several articles online propagating this inaccuracy (i.e., that volatile forces variables to be read from main memory).

    According to the language standard (§17.4):

    A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable

    So informally, all threads will have a view of the most updated value of that variable. There is nothing about how the hardware should enforce such a constraint.

    Why does this happen and code works same with and without volatile

    Well (in your case) without the volatile the code is undefined behavior, meaning you might see or not see the most updated value of the flag won. Consequently, theoretically, the race condition is still there. However, because you have added the following statement

    System.out.println("Player-2 Still Racing...");
    

    in:

    Thread two = new Thread(()->{
                 System.out.println("Player-2 is racing...");
                 while(!won){
                      System.out.println("Player-2 Still Racing...");
                 }
    });
    

    two things will happen: you will avoid the Spin on field problem, and second, if one looks at the System.out.println code:

       public void println(String x) {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    

    One can see that there is a synchronized being called, which will increase the likelihood that the threads will be reading the most updated value of the field won (before the call to the println method). However, even that might change based on the JVM implementation.