Java's multi-threading mechanism series: (4) volatile and instruction reordering (happen-before) that must be mentioned

http://www.cnblogs.com/mengheng/p/3495379.html

First, volatile

volatile is a very old keyword, which was born almost with the birth of JDK. We all know this keyword, but It's not clear when it will be used; we see this keyword all over the JDK and open source frameworks, but concurrency experts often advise us to stay away from it. For example, Thread is a very basic class. The very important thread status field is decorated with volatile. See the code

/* Java thread status for tools,

     * initialized to indicate thread 'not yet started'

     */



    private volatile int threadStatus = 0 ;

As mentioned above, concurrency experts recommend that we stay away from it, especially after the performance of the synchronized keyword in JDK6 has been greatly optimized, and there are almost no scenarios where it is used, but it is still a keyword worth studying. The meaning is not to use it, but to understand it is very helpful to understand the entire multithreading mechanism of Java.
1. Example

Let's first understand the role of volatile, starting with the following code

   1: public class VolatileExample extends Thread{

   2: //Set the class static variable, each thread accesses the same shared variable

   3: private static boolean flag = false;

   4:     

   5: //Infinite loop, wait for the flag to become true before jumping out of the loop

   6: public void run() {while (!flag){};}

   7:     

   8: public static void main(String[] args) throws Exception {

   9: new VolatileExample().start();

  10: //The purpose of sleep is to wait for the thread to start, that is to say, it enters the infinite loop of run

  11: Thread.sleep(100);

  12: flag = true;

  13 : }

  14: }

This example is easy to understand. A thread is started in the main function, and its run method is an infinite loop with flag as the flag. If flag is true then break out of the loop. When main executes to line 12, the flag is set to true. According to the logical analysis, the thread should end at this time, that is, the execution of the entire program is completed.

Execute it and see what the result is? The results are surprising, and the program never ends. The main is definitely ended, the reason is that the run method of the thread has not ended, that is, the flag in the run method is still false.

Add the volatile modifier to line 3, that is,

private static volatile boolean flag = false;

execute it again and see? The result is that the program exits normally, and volatile takes effect.

Let's modify it again. Remove the volatile keyword, restore to the original example, then change while(!flag){} to while(!flag){System.out.println(1);}, and execute it again. According to the analysis, when there is no volatile keyword, the program will not end. Although a print statement is added, but no keyword/logical modification is made, the program should not end, but the execution result is: The program ends normally.

With these perceptual knowledge, let's analyze the semantics of volatile and its role.
2. volatile semantics

The first semantics of volatile is to ensure the visibility of variables between threads. Simply put, when thread A modifies variable X, other threads executing behind thread A can see the change of variable X. In more detail, the following two rules must be met:

    After the thread modifies the variable, it must immediately write back to the main memory.
    When a thread reads a variable, it reads from main memory, not from the cache.

To explain this problem in detail, we have to mention the Java Memory Model (Java Memory Model, JMM for short). Java's memory model is a relatively complex topic, which belongs to the category of Java language specification. Due to limited personal level, I cannot fully explain this matter in limited space. If you want to have a clear understanding, please study "In-depth Understanding of Java Virtual Machine - JVM" Advanced Features and Best Practices" and "The Java Language Specification, Java SE 7 Edition", here is a brief explanation of some sources.

Java needs to define its own memory model in order to ensure its platform and isolate the Java application from the operating system memory model. In the Java memory model, memory is divided into two parts: main memory and working memory. The main memory is shared by all threads, and the working memory is allocated to each thread. The working memory of each thread is independent and mutually exclusive. Invisible, when the thread starts, the virtual machine allocates a piece of working memory for each memory, which not only contains the local variables defined inside the thread, but also the shared variables (objects not constructed within the thread) that the thread needs to use. , that is, in order to improve execution efficiency, reading the copy is faster than reading the main memory directly (here, the main memory can be simply understood as the heap in the virtual machine, and the working memory is understood as the stack (or called the virtual machine stack), the stack It is a continuous small space and is sequentially pushed and popped, while the heap is a discontinuous large space, so the speed of addressing in the stack is much faster than that of the heap). The data exchange between the working memory and the main memory is carried out through the main memory, as shown in the figure below: QQ screenshot 20131228132842

At the same time, the Java memory model also defines a series of interactive operations between the working memory and the main memory and the rules of the order between the operations (This rule is more and more complicated, see "In-depth understanding of Java virtual machine - JVM advanced features and best practices" Chapter 12 12.3.2 section), here only talk about the part related to volatile. For shared ordinary variables, it is agreed that after the variable changes in the working memory, it must be written back to the working memory (sooner or later, but not immediately), but for volatile variables, it is required that after the working memory changes , it must be written back to the working memory immediately, and when the thread reads the volatile variable, it must immediately go to the working memory to get the latest value instead of reading the copy of the local working memory. After the modification of variable X, other threads executing after thread A can see the change of variable X".

Most of the online articles explain volatile so far, but I think there are still omissions, and I will discuss it. Working memory can be said to be a cache of main memory. In order to avoid cache inconsistency, volatile needs to discard this cache. But in addition to the memory cache, there are also caches at the CPU hardware level, namely registers. If thread A modifies the variable X from 0 to 1, the CPU operates in its cache and does not write back to the memory in time, then the JVM cannot see X=1 by the thread B that executes in time, so I think the JVM also uses the hardware-level cache coherence principle when dealing with

volatile variables (for the cache coherence principle of the CPU, see "Java's Multithreading Mechanism Series: (2) Cache Coherence and CAS". Two semantics: prohibit instruction reordering. For instruction reordering, please refer to the "Instruction Reordering" chapter. This is one of the main usage scenarios of volatile at present.
3. Volatile does not guarantee atomicity Compare articles that

introduce volatile does not guarantee atomicity There are many, so I won't give a detailed example here, you can go to the Internet to check the relevant information. In terms of the operation results of multi-threaded concurrent execution of i++, i plus or without volatile is the same, as long as the number of threads is enough, there will be inconsistencies. . Here is the principle of why it cannot guarantee atomicity.

The two semantics of volatile mentioned above ensure the timely visibility of shared variables between threads, but the whole process does not guarantee synchronization (see "Java's Multithreading Mechanism Series: (1) General description of the two characteristics of "lock" in "Basic Concepts"), which is related to the mission of volatile. The background for creating it is that in some cases, it can replace synchronized to achieve the purpose of visibility. Avoid the overhead of thread suspension and scheduling brought by synchronized. If volatile can also guarantee synchronization, then it is a lock and can completely replace synchronized. From this point of view, volatile cannot guarantee synchronization, and it is also based on the above reasons. With the gradual improvement of synchronized performance, volatile gradually withdraws from the historical stage.

Why doesn't volatile guarantee atomicity? Taking i++ as an example, it includes three operations: read, operate, and assign. The following is the operation sequence of two threads. 2

Suppose that thread A is doing i+1, but when no assignment is made, thread B starts to read i , then when thread A assigns i=1 and writes back to the main memory, at this time thread B no longer needs the value of i, but directly hands it to the processor to do +1 operation, so when thread B executes Finished and written back to main memory, the value of i is still 1 instead of the expected 2. In other words, volatile shortens the time difference between the execution of ordinary variables between different threads, but there are still loopholes, and atomicity is still not guaranteed.

It must be mentioned here that, as mentioned at the beginning of this chapter, "the working memory of each thread is independent and invisible to each other. When the thread starts, the virtual machine allocates a piece of working memory for each memory, which not only includes the internal definition of the thread. The local variables also contain copies of the shared variables (objects not constructed within the thread) that the thread needs to use, that is, in order to improve execution efficiency" is not accurate. The example of volatile today is very difficult to reproduce. For example, at the beginning of this article, the effect of volatile is only reflected in the while infinite loop. Even if only a short paragraph such as System.out.println(1) is added, ordinary variables can also achieve The effect of volatile, what is the reason for this? It turns out that only when the frequency of reading variables is high, the virtual machine will not write back to the main memory in time, and when the frequency does not reach the high frequency that the virtual machine thinks, ordinary variables and volatile are the same processing logic. For example, executing System.out.println(1) in each loop increases the time interval for reading variables, so that the virtual machine thinks that the reading frequency is not so high, so the effect of volatile and volatile is achieved (the example at the beginning of this article only Tested on HotSpot24, not tested on other versions of JDK like JRockit). The effect of volatile is easy to reproduce in jdk1.2 and before, but with the continuous optimization of virtual machines, the visibility of ordinary variables today is not such a serious problem, which is why volatile is really not used in scenarios nowadays. Bar.
4. Applicable scenarios of volatile

It makes sense for concurrency experts to recommend that we stay away from volatile, and here's a summary:

    volatile was introduced when synchronized performance was low. Now the efficiency of synchronized has been greatly improved, so the existence of volatile is of little significance.
    Today's non-volatile shared variables have the same effect as volatile-modified variables if the access is not super frequent.
    Volatile does not guarantee atomicity, which is not very clear to everyone, so it is easy to make mistakes.
    volatile can prevent reordering.

So if we're sure we're using volatile correctly, it's a good use case to disable reordering, otherwise we don't need to use it anymore. Only one usage scenario of volatile is listed here, that is, when it is used as a flag (such as the boolean type flag in the example in this article). To use a more professional point of view, it is "the write operation to a variable does not depend on the current value and the variable is not included in the invariant of other specific variables", see "Java Theory and Practice: Correct Use of Volatile Variables" for details.


2. Instruction reordering (happen-before)

Instruction reordering is a more complicated and somewhat unbelievable problem. It also starts with an example (it is recommended that you run the example, this is really reproducible, and the probability of reordering Still quite high), there is a perceptual understanding

/**

* A simple example showing Happen-Before.

* There are two shared variables here: a and flag, the initial values ​​are 0 and false respectively. Give it first in ThreadA a=1, then flag=true.

* If it is in order, then in ThreadB if if (flag) is successful, then a=1, and a is still 1 after a=a*1, and the following if (a==0) should never be If true, it will never print.

* But the actual situation is: in the case of 100 trials, there will be 0 or several printing results, and the results of 1000 trials are more obvious, with more than a dozen printing.

*/

public class SimpleHappenBefore {

    /** This is a variable for verification results*/

    private static int a=0;

    /** This is a flag bit*/

    private static boolean flag=false;

   

    public static void main(String[] args) throws InterruptedException {

        //Because the conclusion of reordering may not be tried in the case of multi-threading, try some more times

        for(int i=0;i<1000;i++){

            ThreadA threadA=new ThreadA();

            ThreadB threadB=new ThreadB( );

            threadA.start();

            threadB.start();

           

            //After waiting for the thread to end, reset the shared variable to make the work of verifying the result easier.

            threadA.join();

            threadB.join();

            a=0;

            flag=false;

        }

    }

   

    static class ThreadA extends Thread{

        public void run(){

            a=1;

            flag=true;

        }

    }

   

    static class ThreadB extends Thread{

        public void run(){

            if(flag){

                a=a*1;

            }

            if(a==0){

                System.out.println("ha,a==0");

            }

        }

    }

} The

example is relatively simple, A note has also been added and is no longer described in detail.



What is instruction reordering? There are two levels:

    At the virtual machine level, in order to minimize the impact of CPU vacancy caused by the memory operation speed being much slower than the CPU running speed, the virtual machine disrupts the program writing order according to some of its own rules (the rules will be described later) - that is Code written later may execute first in chronological order, while code written earlier will execute later - to make the best possible use of the CPU. Take the above example: if it is not the operation of a=1, but a=new byte[1024*1024] (allocating 1M space), then it will run very slowly. At this time, the CPU is waiting for its execution to end. Or should I execute the following flag=true first? Obviously, executing flag=true first can use the CPU in advance and speed up the overall efficiency. Of course, the premise is that no errors will be generated (what kind of errors will be discussed later). Although there are two cases here: the latter code starts executing before the former code; the former code starts executing first, but when the efficiency is slower, the latter code starts executing and ends before the former code execution. Regardless of who starts first, the following code may end first in some cases.
    At the hardware level, the CPU will reorder the received batch of instructions according to its rules, which is also based on the reason that the CPU speed is faster than the cache speed. Reordering within the limited instruction range of the virtual machine, while the virtual machine can be reordered at a larger level, more instruction range. For the hardware reordering mechanism, please refer to "Memory Reordering from JVM Concurrency". Reordering

is very difficult to understand. The above only briefly mentions its scenarios. To better understand this concept, you need to construct some Examples and charts, here are two more detailed and vivid articles "happens-before common explanation" and "in-depth understanding of the Java memory model (2) - reordering". Among them, "as-if-serial" should be mastered, that is: no matter how reordered, the execution result of single-threaded program cannot be changed. The compiler, runtime, and processor must all obey "as-if-serial" semantics. Take a simple example,

public void execute(){

    int a=0;

    int b=1;

    int c=a+b;

}

Here the two sentences a=0 and b=1 can be sorted arbitrarily without affecting the logic result of the program, but the sentence c=a+b must be executed after the first two sentences.



As can be seen from the previous example, the probability of reordering in a multi-threaded environment is still quite high. There are volatile and synchronized on the keywords to disable reordering. In addition, there are some rules, which are precisely these rules , so that we do not feel the disadvantages of reordering in our usual programming work.

    Program Order Rule: In a thread, according to the code order, the operations written in front occur before the operations written in the back. Rather, it should be control flow order rather than code order, because branching, looping, etc. structures are considered.
    Monitor Lock Rule: An unlock operation occurs before a lock operation on the same object lock. The emphasis here is on the same lock, and "later" refers to the sequence in time, such as lock operations that occur in other threads.
    Volatile Variable Rule (Volatile Variable Rule): The write operation to a volatile variable occurs after the read operation of the variable, and the "later" here also refers to the sequence in time.
    Thread Start Rule: Thread's exclusive start() method precedes every action of this thread.
    Thread Termination Rule: Each operation in a thread occurs first in the termination detection of this thread. We can detect that the thread has terminated through the end of the Thread.join() method and the return value of Thread.isAlive(). implement.
    Thread Interruption Rule: The call to the thread interrupt() method takes precedence over the code of the interrupted thread to detect the occurrence of an interrupt event. You can use the Thread.interrupted() method to detect whether the thread has been interrupted.
    Finalizer Rule: The initialization of an object (the end of constructor execution) occurs first at the beginning of its finalize() method.
    Transitivity: 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.

It is the above rules that guarantee the order of happen-before. If the above rules are not met, the execution order cannot be guaranteed to be equal to the code order in a multi-threaded environment, that is, "If you observe in this thread, all operations are Ordered; if you observe another thread in one thread, everything that does not meet the above rules is out of order." Therefore, if our multithreaded program depends on the order of code writing, then we must consider whether the above rules are met, If it does not conform, it must be conformed through some mechanisms, the most commonly used are synchronized, Lock and volatile modifiers.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326569399&siteId=291194637