编程知识 cdmana.com

Read Java Memory Model (JMM) and volatile keyword

What is? JMM Model ?

Java Memory model (Java Memory Model abbreviation JMM) It 's an abstract concept , It doesn't really exist , It describes a set of rules or specifications , Through this set of specifications, the variables in the program are defined ( Include instance fields 、 Static fields and the elements that make up the array object ) Access to .JVM The entity running the program is the thread , And when each thread is created JVM Will create a working memory for it ( Some places are called stack spaces ), Used to store thread private data , and Java The memory model states that all variables are stored in main memory , Its main memory is the shared memory area , All threads have access to , But thread operations on variables ( Read assignments, etc ) Must be done in working memory , First of all, we should test the variables from the main memory to the increased working memory space , Then operate on variables , Write the variables back to main memory after the operation , You can't directly manipulate variables in main memory , Working memory stores copies of the variables in the main memory , Working memory is the private data area of each thread , So different threads can't access each other's working memory , Communication between threads ( Pass value ) It has to be done in main memory .

《2020 newest Java Basic video tutorial and learning route !》

JMM differ JVM Memory area mode

JMM And JVM The division of memory areas is a different conceptual hierarchy , More appropriately JMM It describes a set of rules , Through this set of rules to control the access way of each variable in the shared data area and private data area ,JMM It's about atomicity 、 Orderliness 、 Visibility expansion .JMM And Java The only similarity between memory regions , There are shared data areas and private data areas , stay JMM The main memory belongs to the shared data area , To some extent, heaps and method areas should be included , And working memory data thread private data area , To some extent, it should include program counter 、 Virtual machine stack and local method stack .
Threads 、 The working memory 、 Main memory working interaction diagram ( be based on JMM standard ), as follows :

Main memory

The main storage is Java Instance object , All thread created instance objects are stored in main memory , No matter what Whether the instance object is a member variable or a local variable in a method ( Also known as local variables ), Of course, it also includes shared information 、 Constant 、 Static variables . Because it's a shared data area , Multiple threads accessing the same variable may send thread safety issues .

The working memory

Mainly stores all local variable information of the current method ( The working memory stores a copy of the variables in the main memory ), Each thread can only access its own working memory , In other words, local variables in a thread are invisible to other threads , Even if two threads execute the same code , They also create local variables in their working memory that belong to the current thread , Of course, the bytecode line number indicator is also included 、 relevant Native Method information . Note that because the working memory is private data for each thread , Threads cannot access each other's working memory , So the data stored in working memory is not thread safe .

according to JVM Virtual machine regulates the data storage type and operation mode of main memory and working memory , For a member method in an instance object , If the method includes local variables is the base data type (boolean、type、short、char、int、long、float、double), Will be stored directly in the working memory frame stack , The object instance will be stored in main memory ( Shared data area , Pile up ) in . But for instance object member variables , Whether it's a basic data type or a wrapper type (Integer、Double etc. ) Again reference type , Will be stored in the heap area . as for static The main class information will be stored in memory as well as in the main class itself .
It should be noted that , Instance objects in the main memory can be shared by multithreads , If two threads call the same method of the same object at the same time , Then two threads will copy the data to be operated on to the direct working memory , Refresh to main memory after late operation . The model is shown in the figure below :

Java The relationship between memory model and hardware memory architecture

Through the previous hardware memory architecture 、Java Memory model and Java The realization principle of multithreading , We should have realized that , The execution of multithreading will eventually be mapped to the hardware processor for execution , but Java Memory model and hardware memory architecture are not exactly the same . For hardware memory, there are only registers 、 Cache memory 、 The concept of main memory , No working memory ( Thread private data area ) And main memory ( Heap memory ) Points , in other words Java Memory model partition of memory has no effect on hardware memory , because JMM It's just an abstract concept , It's a set of rules , It doesn't really exist , Whether it's working memory data or main memory data , For computer hardware, it will be stored in the main memory of the computer , Of course, it is also possible to store CPU In a cache or register , So on the whole ,Java Memory model and computer hardware memory architecture is a cross relationship , It's the intersection of abstract concept partition and real physical hardware .( Note that for Java The same is true for memory regions )

JMM The necessity of being

In understanding Java Memory partition 、 Hardware memory architecture 、Java The principle of multithreading and Java After the specific relationship of the memory model , Let's talk about Java The necessity of memory model .

because JVM The entity running the program is the thread , And when each thread is created JVM Will create a working memory for it ( Some places are called stack spaces ), Used to store thread private data , Variable operations in thread and main memory must be done indirectly through working memory , The main process is to copy variables from main memory to each thread 's own working memory space , Then operate on variables , Write the variables back to main memory after the operation , If there are two threads operating on the variables of an instance object in main memory at the same time, it may cause thread safety problems .

Suppose there is a shared variable in main memory x , Now there is A and B The two threads are responsible for this variable x=1 To operate , A/B Shared variable copies exist in the threads' respective working memory x . Suppose now A The thread wants to modify x The value of is 2, and B The thread wants to read x Value , that B The value read by the thread is A Value after thread update 2 Or update the value of money 1 Well ?

The answer is : Not sure . namely B It is possible for a thread to read A Thread updates the value of money 1, It's also possible to read A Value after thread update 2, This is because working memory is a private data area for each thread , And threads A Operating variables x when , The first is to copy variables from main memory to A In the working memory of the thread , Then operate on variables , After the operation is completed, change the variable x Write back to main memory . And for B The thread is similar , This may cause data consistency between main memory and working memory , Suppose it's directly in working memory , such B The value the thread reads is x=1 , But if A The thread has put x=2 Write back to main memory ,B If the thread starts to read , So at this time B What the thread reads is x=2 , But when it arrives, it will be sent first ?

As shown in the figure below, the case :

Above about main memory and working memory direct specific interaction protocol , That is, how to copy a variable from main memory to working memory , How to synchronize from working memory to main memory ,Java The memory model is defined by the following eight operations .

Eight atomic operations of data synchronization

  1. lock( lock ): A variable acting on main memory , Mark a variable as a thread exclusive state ;
  2. unlock( Unlock ): A variable acting on main memory , Release a variable that is locked , Freed variables can be locked by other threads ;
  3. read( Read ): A variable acting on main memory , Transfers a variable value from main memory to the thread's working memory , After that load Work use ;
  4. load( load ): A variable acting on working memory , It is the read Operate the variable value obtained from main memory and put it into working memory ;
  5. use( Use ): A variable acting on working memory , Passes the value of a variable in working memory to the execution engine ;
  6. assign( assignment ): A variable acting on working memory , It assigns a value received from the execution engine to a variable in working memory ;
  7. store( Storage ): A variable acting on working memory , Transfers the value of a variable in working memory to main memory , For subsequent write The operation of ;
  8. wirte( write in ): A variable acting on working memory , It is the store Operations are transferred from a variable value in working memory to a variable in main memory .
  • If you want to copy a variable from main memory to working memory , You need to do it sequentially read and load operation ;
  • If you synchronize variables from working memory to the main memory , You need to do it sequentially store and write operation .

but Java The memory model only requires that the above operations be performed in order , There is no guarantee that the execution must be continuous .

Synchronization rule analysis


  1. A thread is not allowed for no reason ( Nothing happened assign operation ) Synchronize data from working memory back to main memory ;
  2. A new variable can only be created in main memory , It is not allowed to use an uninitialized one directly in working memory (load perhaps assign) The variable of . That is to say, to implement a variable use and store Before the operation , You have to do it yourself first assign and load operation ;
  3. Only one thread is allowed to work on a variable at a time lock operation , but lock The operation can not be repeated multiple times by the same thread , Multiple execution lock after , Only execute the same number of times unlock operation , Variables will be unlocked .lock and unlock They have to come in pairs ;
  4. If you execute on a variable lock operation , Will empty the value of this variable in working memory , Before executing the engine using variables, you need to re execute load or assign The operation initializes the value of the variable ;
  5. If a variable is not previously lock Locking operation , It is not allowed to be executed unlock operation ; I'm not allowed to go unlock A variable that is locked by another thread ;
  6. Execute on a variable unlock Before the operation , You must first synchronize this variable to main memory ( perform store and write operation ).

Atomicity

Atomicity Refers to an operation that cannot be interrupted , Even in a multithreaded environment , Once an operation is started, it will not be affected by other threads .
stay Java in , The operations of reading and assigning values to variables of basic data type are atomic operations. It should be noted that : about 32 Bit system ,long Type data and double Type data ( For basic type data :byte、short、int、float、boolean、char Reading and writing are atomic operations ), Their reading and writing are not atomic , That is to say, if there are two threads working on long Type or double There is interference between reading and writing data of type , Because for 32 Bit virtual machine , Every time atoms read and write is 32 position , and long and double It is 64 Bit storage unit , This will cause a thread to write , Before the operation is completed 32 After the atomic operation of bit , turn B When the thread reads , Just after you get it 32 A data , In this way, a variable that is neither the original value nor the modified value of the thread can be read back , It may be “ Half a variable ” The numerical , namely 64 Bit data is divided into two reads by two threads . But don't worry too much , Because I read “ Half a variable ” The situation is less , At least in today's commercial virtual machines , Almost all of them put 64 Read and write operations of bit data are performed as atomic operations , So don't worry too much about it , Just know what's going on .

X=10; // Atomicity ( Simple reading 、 Assign a number to a variable ) Y = x; // Mutual assignment between variables , It's not an atomic operation  X++; // Calculate variables  X=x+1; 

visibility

I understand Command rearrangement After the phenomenon , Visibility is easy to understand . Visibility refers to when a thread changes the value of a shared variable , Can other threads immediately know the modified value . For serial programs , Visibility doesn't exist , Because we change the value of a variable in any operation , This variable can be read in subsequent operations , And it's a new value that has been modified .

But not necessarily in a multithreaded environment , We analyzed it before , Because the thread's operations on shared variables are copied to their respective working memory before they are written back to the main memory , There may be a thread A Modified shared variables x Value , Before it's written back to main memory , Another thread B For the same shared variable in main memory x To operate , But this time A Shared variables in thread working memory x For threads B It's not visible , This delay in synchronization between working memory and main memory can cause visibility problems , In addition, instruction rearrangement and compiler optimization can lead to visibility problems , Through the previous analysis , We know whether it's compiler optimization or processor optimization rearrangement , In multithreaded environment , It does lead to the problem of program out of order execution , This leads to visibility problems .

Orderliness

Orderliness refers to the execution code of a single thread , We always think that code is executed in sequence , There is nothing wrong with this understanding , Comparison is true for a single thread , But for multithreaded environments , Then disorder may occur , Because after the program is compiled and called machine code instructions, there may be instruction rearrangement , The rearranged instruction may not be in the same order as the original instruction , To understand is , stay Java In the program , If within this thread , All operations are treated as ordered behavior , If it's a multithreaded environment , One thread observes another thread , All operations are unordered , The first half of the sentence refers to the consistency of serial semantic execution within a single thread , In the second half of the sentence, instruction rearrangement and synchronization delay between working memory and main memory are observed

JMM How to solve atomicity 、 Visibility and order issues

Atomic question

except JVM In addition to the atomicity of basic data type read and write operations provided by itself , Can pass synchronized and Lock To achieve atomicity . because synchronized and Lock It can guarantee that only one thread can access the code block at any time .

Visibility issues

volatile Keywords guarantee visibility . When a shared variable is volatile Keyword modification , It ensures that the modified value is immediately visible to other threads , That is, the modified value is immediately updated to main memory , When other threads need to read , It will go to memory to read new values .synchronized and Lock Visibility is also guaranteed , Because they guarantee that only one thread can access the shared resources at any time , And refresh the modified variable into memory before releasing the lock .

The question of order

stay Java Inside , Can pass volatile Keywords to ensure a certain “ Orderliness ”. In addition, you can use synchronized and Lock To ensure order , Obviously ,synchronized and Lock Ensure that only one thread executes synchronization code at each time , So let the threads execute the synchronization code in sequence , Nature guarantees order .

Java Memory model

Each thread has its own working memory , All operations on variables by threads must be performed in working memory , It can't operate on the main memory directly . And each thread cannot access the working memory of other threads .Java The memory model has some innate “ Orderliness ”, That is to say, the order can be guaranteed without any means , This is often called happens-before principle . If the execution order of two operations cannot be from happens-before Derive the principle , Then they can't guarantee their order , Virtual machines can reorder them at will .

Instruction reordering

Java Language specification JVM Sequential semantics are maintained within threads . That is, as long as the final result of the program is equal to that of its sequenced case , Then the execution order of instructions can be inconsistent with the order of code , This process is called reordering instructions .
What is the meaning of instruction reordering ?JVM According to the processing characteristics (CPU Multi level cache 、 Multi core processors, etc ) Reorder machine instructions appropriately , Make the machine instructions more in line with CPU The execution features of , Maximize the performance of the machine .

The following figure shows the sequence of instructions from source code to final execution :

as-if-serial semantics

as-if-serial Semantics means : No matter how reorder ( Compiler and processor to improve parallelism ),( Single thread ) The execution result of the program cannot be changed . compiler 、runtime And the processor must comply as-if-serial semantics .
In order to observe as-if-serial semantics , Compilers and processors do not reorder operations that have data dependencies , Because this reordering will change the execution result . however , If there are no data dependencies between operations , These operations can be reordered by compilers and processors .

happens-before principle

Only by synchronized and volatile Keywords to guarantee atomicity 、 Visibility and order , So writing concurrent programs can be cumbersome , Fortunately, , from JDK 5 Start ,Java Use the new JSR-133 Memory model , Provides happens-before principle To help ensure the atomicity of program execution 、 The issue of visibility and order , It's judging that there is a lot of competition in the data 、 Thread safe sentence .happens-before The principles are as follows :

  1. Principle of program sequence , In other words, semantic serialization must be guaranteed within a thread , That is to say, execute in code order .
  2. The lock rules , Unlock (unlock) Operation must occur in the subsequent locking of the same lock (lock) Before , in other words , If for a lock after unlocking , And lock it up , Then the locking action must be after the unlocking action ( Same lock ).
  3. volatile The rules , volatile Variable writing , It starts with reading , This ensures that volatile Visibility of variables , The simple understanding is ,volatile Every time a variable is accessed by a thread , The value of the variable is forced to be read from main memory , And when the variable changes , It also forces the latest value to be flushed to main memory , Any time , Different threads can always see the latest value of the variable .
  4. Thread start rule , Thread start() Method precedes every action , That is, if the thread A In the thread of execution B Of start Method previously changed the value of the shared variable , So when threads B perform start When the method is used , Threads A Changes to shared variables, to threads B so .
  5. Transitivity ,A Precede B,B Precede C, that A Necessary before C.
  6. Thread termination policy , All operations of a thread precede the end of the thread ,Thread.join() The function of the method is to wait for the currently executing thread to terminate . Suppose you're in a thread B Before the termination , Modified shared variables , Threads A From thread B Of join Method successfully returned , Threads B Changes to shared variables will affect the thread A so .
  7. Thread interrupt rule , For threads interrupt() Method call occurs first in the interrupted thread's code and detects the occurrence of the interrupt event , Can pass Thread.interrupted() Method to detect that the thread is very interrupted .
  8. Object termination rule , Object constructor execution , The end of the prior finalize() Method .
finalize() yes Object The method in , Called before the garbage collector is about to reclaim the memory occupied by the object , When an object is declared dead by the virtual machine, it will be called first finalize() Method , Let this object handle the last thing in its lifetime ( This object can take advantage of this opportunity to break away from the fate of death ).

volatile Memory semantics

volatile yes Java The lightweight synchronization mechanism provided by virtual machine .volatile Keywords have the following two functions :

  1. Guaranteed to be volatile The modified shared variable is always visible to all threads , That is, when a thread is modified by volatile Decorate the value of shared variables , The new value is always immediately known by other threads .
  2. Tight instruction reordering optimization .

volatile The visibility of

About volatile The visibility of the role of , We have to mean to be volatile The modified variable is always immediately visible to all threads , about volatile All other variables can always be written to other threads immediately .
Case study : Threads A change initFlag After the attributes , Threads B Feel immediately

package com.niuh.jmm;

import lombok.extern.slf4j.Slf4j;

/**
 * @description: -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Jmm03_CodeVisibility.refresh
 * -Djava.compiler=NONE
 **/
@Slf4j
public class Jmm03_CodeVisibility {

    private static boolean initFlag = false;

    private volatile static int counter = 0;

    public static void refresh() {
        log.info("refresh data.......");
        initFlag = true;
        log.info("refresh data success.......");
    }

    public static void main(String[] args) {
        //  Threads A
        Thread threadA = new Thread(() -> {
            while (!initFlag) {
                //System.out.println("runing");
                counter++;
            }
            log.info(" Threads :" + Thread.currentThread().getName()
                    + " The current thread sniffs initFlag A change of state ");
        }, "threadA");
        threadA.start();

        //  Sleep in the middle 500hs
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //  Threads B
        Thread threadB = new Thread(() -> {
            refresh();
        }, "threadB");
        threadB.start();
    }
}

Combined with the previous introduction of data synchronization eight atomic operations , Let's analyze :
Threads A After starting

  • First step : perform read operation , For main memory , Put the variable initFlag Copy from main memory , It's not in working memory yet , It's in the bus . Here's the picture
  • The second step : perform load operation , Working memory , The variables copied in the previous step , Put it in working memory ;
  • The third step : perform use( Use ) operation , Working memory , Passing variables in working memory to the execution engine , For threads A Come on , The execution engine will judge initFlag = true Do you ? It's not equal to , The cycle goes on all the time

The execution process is as follows :

Threads B After starting

  • First step : perform read operation , For main memory , Copy from main memory initFlag Variable , At this time, the copied variables are not put into the working memory , This step is to load To prepare for ;
  • The second step : perform load operation , Working memory , Put the copied variables into working memory ;
  • The third step : perform use operation , Working memory , Pass variables from working memory to the execution engine , Perform engine judgment while(!initFlag), So execute the loop body ;
  • Step four : perform assign operation , Working memory , Assign the value received from the execution engine to the working memory variable , Setting the inifFlag = true ;
  • Step five : perform store operation , Working memory , The variables in working memory initFlag = true Pass to main memory ;
  • Step six : perform write operation , Working memory , Write variables to main memory .

volatile There is no guarantee of atomicity

// Example 
public class VolatileVisibility {
    public static volatile int i =0;
    public static void increase(){
        i++;
    }
} 

In a concurrent scenario , i Any change in the variable is immediately reflected in other threads , But there are multiple threads calling at the same time increase() The transformation of methods , There will be thread safety issues , After all i++ Operations are not atomic , The operation is to read the value first , Then write back a new value , Equivalent to the original value plus 1, It's done in two parts . If the second thread reads between the first thread reading the old value and writing back the new value i Value , Then the second thread will see the same value with the first thread , And add the same value 1 operation , This leads to thread safety failure , So for increase Method must use synchronized modification , In order to ensure thread safety , It should be noted that once used synchronized After modifying the method , because sunchronized It is also possessed by volatile The same characteristics , You can see , Therefore, in such a case, it can be completely omitted volatile Modifying variables .
Case study : Got up 10 Threads , Each thread adds to 1000,10 Threads , Is the total 10000

package com.niuh.jmm;

/**
 * volatile Can guarantee visibility ,  Atomicity is not guaranteed 
 */
public class Jmm04_CodeAtomic {

    private volatile static int counter = 0;
    static Object object = new Object();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    synchronized (object) {
                        counter++;// three -  read , Self adding , Write back to 
                    }
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter);

    }
} 

And the actual result is , Less than 10000, as a result of : There are concurrent operations . Now , If I were in counter Add keywords volatile, Can we guarantee atomicity ?

private volatile static int counter = 0; 

We found that , Still not 10000, This explanation volatile Atomicity is not guaranteed .

Every thread , There's only one operation , counter++, Why can't we guarantee atomicity ?

Actually counter++ Not in one step . He did it in multiple steps . Let's use the following diagram to explain

Threads A adopt read, load Load variables into working memory , adopt user Send variables to the execution engine , Execution engine execution counter++, When a thread B Launched the , adopt read, load Load variables into working memory , adopt user Send variables to the execution engine , Then perform the copy operation assign, stroe, write operation . We see that this is going through n A step . Although it seems to be a simple sentence .
When a thread B perform store When you pass data back to main memory , At the same time, the thread will be informed of A, discarded counter++, And then counter It's self adding 1, Will be self adding counter lose , This leads to less total data 1.

volatile No reordering

volatile Another role of keywords is to prevent instruction rearrangement optimization , In order to avoid the multi-threaded environment, the program can be executed out of order , The optimization of instruction rearrangement has been analyzed before , Here is a brief description of volatile How to realize the optimization of forbidden instruction rearrangement . First understand a concept , Memory barrier (Memory Barrier)

Hardware layer memory barrier

Intel Hardware provides a series of memory barriers , The main thing is :

  1. ifence, It's a kind of Load Barrier Reading barrier ;
  2. sfence, It's a kind of Store Barrier Write barriers ;
  3. mfence, It's an all-around barrier , Have ifence and sfence The ability of ;
  4. Lock Prefix ,Lock It's not a memory barrier , But it can do something like a memory barrier .Lock Would be right CPU Bus and cache locking , It can be understood as CPU An instruction level lock . It can be followed by ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD、and XCHG Such as instruction .

JVM The memory barrier of

Different hardware implements memory barrier in different ways ,Java Memory models shield the differences between these underlying hardware platforms , from JVM To produce corresponding machine codes for different platforms .JVM Four types of memory barrier instructions are provided in :

Memory barrier , also called memory barriers , It's a CPU Instructions , It has two functions :

  1. One is to ensure the execution sequence of specific operations ;
  2. The second is to ensure the memory visibility of some variables ( Use this feature to achieve volatile Memory visibility ).

Because both compiler and processor can perform instruction rearrangement optimization . If you insert a Memory Barrier It's a high-speed compiler and CPU, No matter what the order is, it can't be with this Memory Barrier Instruction reordering , In other words, by inserting the memory barrier, the instructions before and after the memory barrier are forbidden to perform reordering optimization .
Memory Barrier Another function of the brush is to force all kinds of CPU Cache data , So anything CPU All threads on can read the latest version of these data .
All in all ,volatile It is through the memory barrier that variables realize their semantics in memory , Visibility and no reordering optimization .
Let's look at a very typical example of no reordering optimization DCL, as follows :

public class DoubleCheckLock {
    private volatile static DoubleCheckLock instance;
    private DoubleCheckLock(){}
    public static DoubleCheckLock getInstance(){
        // The first test 
        if (instance==null){
            // Sync 
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    // Where problems may arise in a multithreaded environment 
                    instance = new  DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

The above code is a classic singleton of double detection code , This code has no problem in a single threaded environment , But if in the multithreading environment, there may be thread safety problems . Because it is because a thread performs the first detection , Read instance Not for null when ,instance The reference object of may not be initialized yet .

because instance = new DoubleCheckLock(); It can be divided into the following 3 Step through ( Pseudo code )

memory = allocate(); // 1. Allocate object memory space 
instance(memory); // 2. Initialize object 
instance = memory; // 3. Set up instance Points to the memory address just allocated , here instance != null 

Because of the steps 1 And steps 2 It's possible to reorder , as follows :

memory=allocate();//1. Allocate object memory space 
instance=memory;//3. Set up instance Points to the memory address just allocated , here instance!=null, But the object hasn't been initialized yet !
instance(memory);//2. Initialize object  

Because of the steps 2 And steps 3 There is no data dependency , Moreover, no matter before or after the rearrangement, the program's pointing result does not change in a single thread , So this rearrangement optimization is allowed . However, instruction rearrangement only guarantees the consistency of serial semantic execution ( Single thread ), But it doesn't care about semantic consistency between threads . So when a thread accesses instance Not for null when , because instance The instance may not have been initialized , This leads to thread safety problems . So how to solve it , It's simple , We use volatile prohibit instance Variables are optimized by the execution of instructions .

// Disable instruction rearrangement optimization 
private volatile static DoubleCheckLock instance; 

volatile Implementation of memory semantics

As mentioned earlier, over ordering is divided into Compiler reorder and Processor reorder . In order to realize volatile Memory semantics ,JMM The two types of reordering types are limited respectively .

Here is JMM For the compiler volatile Reorder rule table .

for instance , The last cell in the second row means : In the program , When the first operation is read or written to a common variable , If the second act volatile Write , The compiler cannot reorder the two operations .
As can be seen from the above figure :

  • When the second operation is volatile Writing time , No matter what the first operation is , Can't reorder . This rule ensures that volatile Operations before writing will not be reordered by the compiler to volatile After writing .
  • When the first operation is volatile Reading time , No matter what the second operation is , Can't reorder . This rule ensures that volatile Operations after reading will not be reordered by the compiler to volatie Before reading .
  • When the first operation is volatile Write , The second operation is volatile When reading or writing , Cannot reorder .

In order to achieve volatile Memory semantics of , When compiling bytecode , A memory barrier is inserted in the instruction sequence to prevent a particular type of processor reordering . For compilers , It is almost impossible to find an optimal arrangement to minimize the total number of inserted barriers . So ,JMM Take a conservative strategy . The following is based on the conservative strategy JMM Memory barrier insertion strategy .

  • At every volatile Insert a... In front of the write operation StoreStore barrier ;
  • At every volatile Insert a... After the write operation StoreLoad barrier ;
  • At every volatile Insert a... After the read operation LoadLoad barrier ;
  • At every volatile Insert a... After the read operation LoadStore barrier ;

The above memory barrier insertion strategy is very conservative , But it can be guaranteed on any processor platform , You can get the right... In any program volatile Memory semantics .

Here's the conservative strategy ,volatile Write insert Diagram of instruction sequence generated after memory barrier

Above picture StoreStore The barrier can guarantee that in volatile Before writing , All the previous normal writes are already visible to any processor . This is because StoreStore The barrier will protect all the common things on it volatile Refresh to main memory before writing .
What's more interesting here is ,volatile Write the following StoreLoad barrier . The purpose of this barrier is to avoid volatile Write and there may be volatile read / Write operation reorder . Because the compiler often can't accurately judge in a volatile It is very necessary to insert a StoreLoad barrier ( such as , One volatile After writing, the method immediately return). In order to ensure the correct implementation of volatile Memory semantics of ,JMM In adopting a conservative strategy : At every volatile At the end of it , Or each volatile Insert a before reading StoreLoad barrier . From the perspective of overall execution efficiency ,JMM Finally chose in each volatile Insert a StoreLoad barrier , because volatile Write - The common usage patterns of read memory semantics are : A write thread writes volatile Variable , Multiple threads read the same volatile Variable . When the number of read threads greatly exceeds the number of write threads , Choice in volatile Insert... After writing StoreLoad The barrier will bring about a considerable improvement in execution efficiency . You can see it here JMM A feature in implementation : First, make sure it's right , Then go after the efficiency of execution .

The picture below is under conservative strategy ,volatile Read insert Diagram of instruction sequence generated after memory barrier

Above picture LoadLoad The barrier is used to prevent the processor from putting the volatile read With the following normal read reorder .LoadStore The barrier is used to prevent the processor from putting the volatile Read with the following normal write reorder .
Above volatile Write and volatile Read memory barrier insertion strategy is very conservative . In actual execution , As long as it doesn't change volatile Write - Memory semantics of reading , The compiler can omit unnecessary barriers depending on the situation .
The following is illustrated by specific example code .

class VolatileBarrierExample {
       int a;
       volatile int v1 = 1;
       volatile int v2 = 2;
       void readAndWrite() {
           int i = v1;      //  first volatile read 
           int j = v2;       //  the second volatile read 
           a = i + j;         //  Common writing 
           v1 = i + 1;       //  first volatile Write 
           v2 = j * 2;       //  the second  volatile Write 
       }
} 

in the light of readAndWrite() Method , The compiler can do the following optimizations when generating bytecode .

Be careful , final StoreLoad Barriers cannot be omitted . Because the second one volatile After writing , Methods immediately return. At this point, the compiler may not be able to accurately determine that there will be volatile Read or write , For safety's sake , The compiler usually inserts a StoreLoad barrier .
The above optimization is for any processor platform , Because different processors are different “ Tightness ” The processor memory model , The insertion of memory barrier can also be optimized according to the specific processor memory model . With X86 After processing, for example , Except for the last StoreLoad Outside the barrier , Other barriers will be omitted .
Under the conservative strategy volatile Read and write , stay X86 The processor platform can be optimized as shown in the figure below .X86 The processor will only read to - Reorder write operations .X86 Can't read - read 、 read - Write and Write - Write Reorder , So in X86 This will be omitted from the processor 3 Memory barrier for each operation type . stay X86 in ,JMM Only in the volatile Insert a StoreLoad The barrier can be realized correctly volatile Write - Memory semantics of reading , This means that X86 In the processor ,volatile The cost of writing is more than volatile Reading costs a lot more ( Because execution StoreLoad The cost of the barrier will be higher ).

Reference material

  • 《 The art of concurrent programming 》

版权声明
本文为[Java Sieger]所创,转载请带上原文链接,感谢

Scroll to Top