JAVA multithreading foundation ---------- volatile variable

catalogue

JAVA Memory Model (JMM)

To gain insight into volatile variables, you must first understand the Java memory model. Therefore, before introducing volatile variables, let's briefly understand the Java memory model. Here, we compare the memory model of physical machine (shared memory multi-core system) with JMM, because there is a strong similarity between them.
Physical machine memory model

We know that cache based storage interaction solves the contradiction between processor and memory speed, but it also brings higher complexity to the computer system, that is, cache consistency. When the operation tasks of multiple processors involve the same main memory area, their cache data may be inconsistent. If this happens, whose cache data should be used when synchronizing back to main memory? In order to solve this problem, each processor needs to abide by some protocols when accessing the cache. We won't introduce them one by one here. We just want to better understand JMM.
JMM (Java memory model)

From these two figures, we can see that the architecture of the Java memory model is highly similar to that of the physical machine.
Java Memory Model Specification
1. All variables (a) are stored in main memory (b)
a. The variables here are slightly different from the variables we usually say in the program. The variables here include instance fields, static fields and elements constituting the array object * * (variables that may be accessed by multiple threads), but do not include local variables and method parameters (because they are thread private and will not be shared)
b. Although the name of the main memory here is the same as that of the physical machine, it is not the same concept. The main memory here is only a part of the memory allocated to the Java virtual machine
2. Each thread also has its own working memory (a)
a. The main memory copy * * of the variables used by the thread is saved in the working memory of the thread. All operations of the thread on variables must be carried out in the working memory, and the data in the main memory cannot be read or written directly.
**Main memory copy: if a thread accesses a large object, the thread will not copy the whole object to its own working memory. It may copy the reference of the object and the accessed fields in the object

Interaction between main memory and working memory

(this part is excerpted from understanding Java virtual machine. Write it yourself to deepen your understanding. If you have time, it is still helpful to understand the working mode of JAVA memory model. If you don't have time, skip this part and come back where it is useful. This will not affect our understanding of volatile variables.)

When you understand the basic architecture of the Java memory model, you will naturally think of a question: how does the main memory interact with the working memory, and how to avoid inconsistencies? Let's introduce the interaction between main memory and working memory.
The Java memory model defines the following eight atomic operations (with the exception of long and double variables) to complete the interaction between main memory and working memory

  • lock: acts on variables in main memory, identifying a variable as a thread exclusive state
  • unlock: it acts on the variables in the main memory and releases a locked variable. The released variable can only be locked by other threads
  • read: acts on a variable in main memory and transfers the value of a variable from main memory to the working memory of the thread
  • load: acts on the variables in the working memory, and puts the variable value obtained from the main memory by the read operation into the variable copy of the working memory
  • Use: acts on a variable in the working memory and passes the value of a variable in the working memory to the execution engine. This operation will be executed whenever the virtual machine encounters a bytecode instruction that needs to use the value of the variable
  • assign: it acts on a variable in the working memory and assigns a value received from the execution engine to the variable in the working memory. This operation will be executed whenever the virtual machine encounters a bytecode instruction that assigns a value to the variable
  • store: acts on variables in working memory and transfers the value of a variable in working memory to main memory
  • write: a variable that acts on the main memory and puts the value of the variable obtained from the working memory by the store operation into the variable of the main memory
  • One of the read and load, store and write operations is not allowed to occur separately, that is, a variable is not allowed to be read from the main memory but not accepted by the working memory, or the working memory initiates a write back but not accepted by the main memory
  • A thread is not allowed to synchronize data from the thread's working memory back to the main memory for no reason (no assign operation has occurred)
  • A new variable can only be created in main memory. Before implementing use and store on a variable, you must first perform load and assign operations
  • Only one thread is allowed to lock a variable at the same time, but the lock operation can be repeated by the same thread many times. After locking for many times, the variable will be unlocked only after unlocking for the same number of times
  • If you lock a variable, the value of this variable in the working memory will be cleared. Before the execution engine uses this variable, you need to re execute the load or assign operation to initialize the variable
  • Before unlock ing a variable, the variable must be synchronized back to main memory (store and write operations)
  • If a variable is not locked by the lock operation in advance, it is not allowed to unlock it, nor is it allowed to unlock a variable locked by other threads

Characteristics and usage scenarios of Volatile variable

Through the above simple understanding of the Java memory model, we begin to introduce the Volatile variable
Two properties of Volatile variables

  • Visibility: when a thread modifies the volatile variable, the value of the volatile variable will be written to main memory immediately, and the new value will be refreshed from main memory every time other threads use the volatile variable
  • Prohibit instruction reordering optimization
    Instruction reordering: reordering instructions under the condition of ensuring data dependency in order to improve performance.
a = 1;
b = 2;
c = 3;
d = 4;   //d is a volatile variable
e = 5;
f = 6;

In the above example, codes after line 4 (assigning values to volatile variables) cannot be executed before line 4, codes before line 4 cannot be executed after line 4, and other lines can be reordered. This is the prohibition of instruction reordering of volatile variables

Let's explain these two features with examples
visibility

public class Test{
 private boolean flag = false;
 public void change(){
     flag = true;
 }
 public void doWork(){
     while(!flag){
         ............
     }
 }
}

If thread A is executing doWork and thread B executes change to change the flag status to true, thread A will not immediately exit the loop, because thread B modifies the flag in its working memory and will not immediately write back to main memory
Solution: define flag as a volatile variable

public class Test{
 private volatile boolean flag = false;
 public void change(){
     flag = true;
 }
 public void doWork(){
     while(!flag){
         ............
     }
 }
}

Instruction reordering

public class Test{
   private char[] configText;
   private boolean init = false;
   //Suppose the following code executes in thread A
   public void configer(){
       configText = readConfigFile();    //Code 1
       init = true;   //Notify other threads to configure available code 2
   }
   //Suppose the following code is executed in thread B
   public void work() throws Exception{
       while(!init){   
          Thread.sleep(10000);
       }
       use(configText);
   }
}

In the above program, the actual execution order of code 1 and code 2 may be exchanged. This will cause other threads to start using the configuration information before the configuration information is fully configured, which is obviously incorrect

Solution: define the init variable as volatile

public class Test{
   private char[] configText;
   private volatile boolean init = false;
   //Suppose the following code executes in thread A
   public void configer(){
       configText = readConfigFile();    //Code 1
       init = true;   //Notify other threads to configure available code 2
   }
   //Suppose the following code is executed in thread B
   public void work() throws Exception{
       while(!init){   
          Thread.sleep(10000);
       }
       use(configText);
   }
}

It is worth noting that concurrency security cannot be guaranteed when multiple threads perform non atomic operations on volatile variables
Imagine a scenario

Start 10 threads. Each thread increments volatile variable inc 1000 times and adds a timing thread. The expected effect should be 10000, while the actual output value is 6880, which is a value less than 10000, which does not achieve the expected effect

Look at the code

package hgh0808;
public class Test {
    public static void main(String[] args){
        for(int i = 0;i < 10;i++){
            Thread th = new Thread(new CThread());
            th.start();
        }
        TimeThread tt = new TimeThread();
        tt.start();
        try{
            Thread.sleep(21000);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println(INS.inc);
    }
}
---------------------------------------------------------------------
package hgh0808;
import java.util.concurrent.atomic.*;
public class TimeThread extends Thread{
    @Override
    public void run(){
        int count = 1;
        for(int i = 0;i < 20;i++){
            try{
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(count++);
        }
    }
}
---------------------------------------------------------------------
package hgh0808;
public class CThread implements Runnable{
    @Override
    public void run(){
        for(int j = 0;j < 1000;j++){
            INS.increase();
        }
    }
}
---------------------------------------------------------------------
package hgh0808;
public class INS{
    public static volatile int inc = 0;
    public static void increase(){
            inc++;
    }
}
=====================================================================
Execution results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6880

The reason for this is that the volatile keyword only ensures that the value of inc is correct when each thread starts to use it. However, since the auto increment operation is not atomic, other threads may have modified the value of inc during the execution of the auto increment operation

From the above examples, we can see that locking is not required to ensure atomicity only when volatile variables are used in scenarios that meet the following two conditions

  • Writing to a variable does not depend on the current value of the variable, or only a single thread can modify the value of the variable
  • This variable is not included in the invariance condition with other state variables

Underlying implementation principle of two characteristics of Volatile variable

Above, we introduced the two features of volatile variables and some common usage scenarios. Next, we discuss how Java implements these two features of volatile variables.
For better understanding, we introduce an example

public class Single {
    private volatile static Single instance;
    private Single(){}
    public static Single getInstance(){
        if(instance == null){
            synchronized (Single.class){
                if(instance == null){
                    instance = new Single();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args){
        getInstance();
    }
}

It can be seen from the assembly language that a lock addl lock ADDL $0x0, (% RSP) x0,(%rsp) instruction will be added after assigning a value to the volatile variable

lock addl Lock ADDL $0x0, (% RSP).x0,(%rsp). This operation acts as a memory barrier

  • The function of lock prefix is to write the cache of this processor into memory. This write action will also cause other processors or other cores to invalidate their cache. Therefore, through such an operation, the modification of volatile variables by a thread can be immediately visible to other processors, which is the principle of visibility
  • Instruction reordering means that the processor must be able to correctly handle the instruction dependency to ensure that the program can get the correct execution results.
    For example, the order of the following two statements cannot be exchanged, which will affect the correctness of the program
b+=1;
b*=2;

So in the same thread, the reordered code still looks orderly. Therefore, the lock ADDL $0x0 (% RSP) instruction writes the modification to the memory, which means that all previous operations have been completed, resulting in the effect that "the instruction cannot cross the memory barrier", which is the principle of prohibiting instruction reordering

Keywords: Java jvm Multithreading

Added by awiedman on Thu, 06 Jan 2022 04:23:37 +0200