Visibility of volatile, prevention of instruction reordering, and solutions that do not guarantee atomicity

Preface

The use of volatile is closely related to thread safety. Its main function is to make variables visible among multiple threads, and to prevent instruction rearrangement.

For example, there is a variable a=0 in the main memory, and thread 1 sets a=10. When thread 2 operates a again, it operates on the basis of a=10. Otherwise, it will affect the logic!


Visibility of volatile

To understand the visibility of volatile, you first need to understand the java Memory Model:

java Memory Model

JAVA memory model is defined by Java virtual machine specification, which is used to shield the hardware differences of each platform. In short:

1. All variables are stored in main memory.

2. Each thread has its own working memory, which holds a copy of the variables used by the thread in the main memory.

3. The thread cannot directly read and write variables in main memory, and all operations are completed in working memory.

The interaction between thread, main memory and working memory is shown in the figure below


As shown in the following code, after rt is started, change the value of isRunning to false. At this time, the while loop will not stop because the changed isRunning cannot be found in the run method.

Solution: use volatile to decorate isRunning, so that when the value of isRunning changes, it will immediately refresh to the main memory, and the working memory can immediately get new values

public class RunThread extends Thread {
 private boolean isRunning = true;
 private void setRunning(boolean isRunning){
   this.isRunning = isRunning;
 }
 
 public void run () {
   System.out.println("Get into run Method");
   while(isRunning == true){
     //...
   }
   System.out.println("Thread stop");
 }
 public static void main(String[] args) {
   RunThread rt = new RunThread();
   rt.start();
   try {
     Thread.sleep(3000);
     rt.setRunning(false);
     System.out.println("isRunning The value of has been set to false");
     Thread.sleep(1000);
     System.out.println(rt.isRunning);
   } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
   }
 }
}


volatile prevents instruction rearrangement

As shown in the following code, this is the double check lock writing method of the singleton mode

public class SingletonTest {
   private volatile static SingletonTest instance = null;
   private SingletonTest() { }
   public static SingletonTest getInstance() {
       if(instance == null) {
           synchronized (SingletonTest.class){
               if(instance == null) {
                   instance = new SingletonTest();  //Nonatomic operation
               }
           }
       }
       return instance;
   }
}

We see that instance is decorated with volatile, because instance = new singleton test(); it can be decomposed into:

1.memory =allocate(); //Allocate memory space for objects
2.ctorInstance(memory); //Initializing objects
3.instance =memory; //Set instance to point to the newly allocated memory address

Operation 2 depends on 1, but operation 3 does not depend on 2, so the order of 1, 3 and 2 may appear. When this order appears, although instance is not empty, the object may not be initialized correctly, and an error may occur.

After using volatile to decorate instance, there will be no disorder!


volatile does not guarantee atomicity and Solutions

1. What is atomicity?

Which of the following statements are atomic operations?

x = 10;         //Statement 1
y = x;          //Statement 2
x++;            //Statement 3
x = x + 1;      //Statement 4

Statement 1 is to assign value 10 to x directly, that is to say, the thread executing this statement will write value 10 to working memory directly;

Statement 2 actually contains two operations. It first reads the value of X, and then writes the value of X to working memory. Although the two operations of reading x value and writing x value to working memory are atomic operations, they are not atomic operations together;

Similarly, x + + and x = x+1 include three operations: reading the value of X, adding 1, and writing a new value.

Only the operation of statement 1 is atomic. That is to say, only simple reading and assignment (and must assign a number to a variable, the mutual assignment between variables is not atomic operation) is atomic operation!

2. example

As shown in the following code, count, a variable decorated with volatile, is used to operate count with 10 threads. According to the above statement, + + operation is not an atomic operation! Add 1000 numbers to each thread, one of the printed results must be 10000, but it's not true! Because volatile doesn't guarantee atomicity!

public class VolatileNoAtomic extends Thread{
 private static volatile int count;
//  private static AtomicInteger count = new AtomicInteger(0);
 private static void addCount(){
   for (int i = 0; i < 1000; i++) {
     count ++;
//      count.incrementAndGet();
   }
   System.out.println(count);
 }
 public void run(){
   addCount();
 }
 
 public static void main(String[] args) {
   VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
   for (int i = 0; i < arr.length; i++) {
     arr[i] = new VolatileNoAtomic();
   }
   
   for (int i = 0; i < arr.length; i++) {
     arr[i].start();
   }
 }
}

Solve:

Method 1: use a series of objects of Atomic class Atomic class, so as not to block, but also to ensure atomicity!

Method 2: use synchronized to modify the addCount method. In this way, threads will block after synchronization, run longer, and volatile will fail. This is not recommended

Method 3: Lock with Lock. Of course, it is blocked like method 2


Keywords: Programming Java

Added by Crystal Dragon on Sun, 15 Mar 2020 05:35:22 +0200