21 JVM memory model (JMM)

memory model*

☁️ JMM is the JAVA memory model (java memory model);

☁️ Different hardware manufacturers and different operating systems have different memory access logic. If the code written in a certain system environment runs well and is thread-safe, various problems may occur if the system is changed ;

☁️ The main goal of the Java memory model is to define the access rules of each variable in the program, that is, the underlying details of storing variables into memory and taking variables out of memory in the JVM;

☁️ The variables here include instance fields, static fields, and elements that constitute array objects, but not local variables and method parameters, because the latter two are private to threads and will not be shared by threads;

main memory and working memory

☁️ The Java memory model stipulates that all variables are stored in main memory;

☁️ Each thread also has its own working memory. The thread's working memory stores a copy of the main memory copy of the variables used by the thread. All operations on variables (reading, assignment, etc.) by the thread must be performed in the working memory , but cannot directly read and write variables in the main memory;

☁️ Different threads cannot directly access the variables in each other's working memory, and the transfer of variable values ​​between threads needs to be completed through the main memory; the interactive relationship among threads, main memory, and working memory is as follows:
insert image description here

memory interworking

☁️ There are 8 types of memory interaction operations, and the virtual machine implementation must ensure that each operation is indivisible;

  • lock (lock): a variable that acts on the main memory, marking a variable as a thread-exclusive state;
  • unlock (unlock): A variable that acts on the main memory, it releases a variable that is in a locked state, and the released variable can be locked by other threads;
  • read (read): acts on the main memory variable, it transfers the value of a variable from the main memory to the working memory of the thread, so that the subsequent load action can be used;
  • load (load): a variable that acts on the working memory, it puts the read operation from the variable in the main memory into the working memory;
  • use (use): acts on the variables in the working memory, it transfers the variables in the working memory to the execution engine, whenever the virtual machine encounters a value that needs to use the variable, it will use this instruction;
  • Assign (assignment): acts on variables in the working memory, it puts a value received from the execution engine into the variable copy of the working memory;
  • Store (storage): acts on variables in the main memory, which transfers the value of a variable from the working memory to the main memory for subsequent write use;
  • write (write): acts on the variable in the main memory, it puts the value of the variable obtained by the store operation from the working memory into the variable in the main memory;

☁️ Three characteristics of the Java memory model:

⚡️ Atomicity: The atomic variable operations directly guaranteed by the Java memory model include read, load, assign, use, store, and read; it can be roughly considered that the access to read and write of basic data types is atomic; if more The atomicity of the range needs to be constrained by the synchronized keyword; (that is, one operation or multiple operations are either executed in full and the execution process will not be interrupted by any factors, or are not executed at all);

⚡️ Visibility: Visibility means that when a thread modifies the value of a shared variable, other threads can immediately know the modification; the three keywords volatile, synchronized, and final can achieve visibility;

⚡️ Orderliness: If observed in this thread, all operations are ordered; if observed in another thread, all operations are out of order; the first half of the sentence refers to "the performance in the thread is serial ", the second half of the sentence refers to the phenomenon of "reordering of instructions" and "synchronization delay between working memory and main memory";

☁️ JMM has some innate "orderliness", that is, the orderliness that can be guaranteed without any means. This is usually called the happens-before principle; if the execution order of two operations cannot be obtained from the happens-before principle Deduced, then it proves that their order cannot be guaranteed:

  • Program order rule: In a thread, according to the code order, the operation written in the front happens before the operation written in the back
  • Locking rules: An unLock operation occurs first before a lock operation on the same lock
  • The volatile variable rule: a write operation to a variable happens before a read operation on the variable occurs later
  • Transfer rule: If operation A happens before operation B, and operation B happens before operation C, then it can be concluded that operation A happens before operation C
  • Thread start rule: The start() method of the Thread object occurs first in every action of this thread
  • Thread Interruption Rules: The call to the thread interrupt() method happens before the code of the interrupted thread detects the occurrence of an interrupt event
  • Thread termination rules: All operations in the thread occur before the termination detection of the thread. We can detect that the thread has terminated execution by means of the Thread.join() method ending and the return value of Thread.isAlive()
  • Object finalization rule: the completion of an object's initialization happens before the start of its finalize() method

☁️ That is to say, in order to execute concurrent programs correctly, atomicity, visibility, and order must be guaranteed. As long as one of them is not guaranteed, it may cause the program to run incorrectly;

Special rules for volatile variables

☁️ Variables defined by volatile have two characteristics:

⚡️Guarantee the visibility of this variable to all threads : when a thread modifies the value of this variable, the new value can be immediately known to other threads; ordinary variables cannot do this, the value of ordinary variables is in the thread The inter-transfer needs to be completed through the main memory;

volatile变量在各个线程中是一致的,但是 volatile变量的运算在并发下一样是不安全的 ,原因在于Java里面的运算并非原子操作,例如:

package com.company;
public class Main {
    
    
    public static volatile int num = 0;
    public static void increase() {
    
    
        num++;
    }
    public static void main(String[] args) {
    
    
       Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
    
    
            threads[i] = new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    for (int j = 0; j < 100; j++) {
    
    
                        increase();
                   }
               }
           });
            threads[i].start();
       }
        while (Thread.activeCount() > 2) {
    
    
            Thread.yield();
       }
        System.out.println(num);
   }
}

⚡️ The volatile keyword ensures that the value of num is correct when taking the value, but when num+1 is executed, other threads may have increased the value of num, so that after +1, the smaller value will be synchronized back to in main memory;

⚡️ Since the volatile keyword only guarantees visibility, we still need to use locks (synchronized or lock) to ensure atomicity in computing scenarios that do not meet the following two rules:

1. The result of the operation does not depend on the current value of the variable, or it can ensure that only a single thread modifies the value of the variable;

2. Variables do not need to participate in invariant constraints with other state variables;

⚡️ Scenarios such as the following code are especially suitable for using volatile to control concurrency. When the shutdown() method is called, it can ensure that the doWork() methods executed in all threads are stopped immediately:

volatile boolean shutdownRequested;
public void shutdown() {
    
    
    shutdownRequested = true;
}
public void work() {
    
    
    while(!shutdownRequested) {
    
    
        //do stuff
   }
}

⚡️The semantics of using volatile variables is to prohibit the reordering of instructions . Ordinary variables only guarantee that the correct results can be obtained in all places that depend on the assignment results during the execution of the method, but cannot guarantee the order of variable assignment operations and program code. in the same order of execution;

⚡️ The volatile keyword prohibits instruction reordering with two meanings:

1. When the program executes the read operation or write operation of the volatile variable, all the changes of the previous operations must have been carried out, and the results have been visible to the subsequent operations; the subsequent operations must not have been carried out;

2. When performing instruction optimization, the statement that accesses the volatile variable cannot be executed behind it, nor can the statement behind the volatile variable be executed before it;

⚡️ Such as:

//x、y为非volatile变量
//flag为volatile变量
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

⚡️ Since the flag variable is a volatile variable, statement 3 will not be placed before statement 1 and statement 2, and statement 3 will not be placed after statement 4 and statement 5 during the instruction reordering process. However, it should be noted that the order of statement 1 and statement 2, and the order of statement 4 and statement 5 are not guaranteed;

⚡️ And the volatile keyword can guarantee that when statement 3 is executed, statement 1 and statement 2 must be executed, and the execution results of statement 1 and statement 2 are visible to statement 3, statement 4, and statement 5;

⚡️ Directive reordering:

Map configOptions;
char[] configText;
volatile boolean initialized = false;
//假设以下代码在线程A执行
//模拟读取配置文件信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;
//假设以下代码在线程B执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized) {
    
    
    sleep();
}
//使用线程A初始化好的配置信息
doSomethingWithConfig();

Double Check in singleton mode

☁️ The double checked locking pattern is a method of locking using synchronized blocks. Programmers call it double-checked locking because there will be two checks for instance == null, one outside the synchronized block and one inside the synchronized block. Why is there another check in the synchronized block? Because there may be multiple threads entering the if outside the synchronization block together, multiple instances will be generated if a second check is not performed in the synchronization block;

public static Singleton getSingleton(){
    
    
        if(instance==null){
    
     //Single Checked
 synchronized (Singleton.class){
    
    
       if(instance==null){
    
     //Double Checked
       instance=new Singleton();
       } 
       }
   }
        return instance;
}

☁️ The above code is instance = new Singleton()here. This is not an atomic operation. In fact, this sentence probably does the following three things in the JVM. Allocate memory to instance Call the constructor of Singleton to initialize member variables Point the instance object to the allocated memory space (the instance will be non-null after executing this step) But there is an optimization of instruction reordering in the JVM's just-in-time compiler. That is to say, the order of the second and third steps above is not guaranteed, and the final execution order may be 1-2-3 or 1-3-2. If it is the latter, it is preempted by thread 2 before 3 is executed and 2 is not executed. At this time, instance is already non-null (but it has not been initialized), so thread 2 will directly return instance, and then use it, and then logically error. We only need to declare the instance variable as volatile;

class Singleton{
    
    
    // 确保产生的对象完整性
    private volatile static Singleton instance = null;
    private Singleton() {
    
    }
    public static Singleton getInstance() {
    
    
        if(instance==null) {
    
     // 检查对象是否初始化
            synchronized (Singleton.class) {
    
    
                if(instance==null) // 确保多线程情况下对象只有一个
                    instance = new Singleton();
           }
       }
        return instance;
   }
}

Guess you like

Origin blog.csdn.net/m0_60266328/article/details/125896605
Recommended