java multithreading - volatile

This is the third part of java multithreading:

"java multithreading - how threads come"

"java multithreading - memory model"

The previous article "java multithreading - memory model" has explained the three characteristics of java threads and the happens-before principle. This article mainly explains the principle and application of volatile. After reading this article, you will be familiar with volatile. Application principles and use boundaries will have a deeper understanding. The main content of this article:

  1. Volatile read-write synchronization principle
  2. volatile reordering principle
  3. volatile application

 

The keyword volatile is a lightweight synchronization mechanism provided by jvm, but it is not easy to understand, and it is not used in most cases. It is abandoned by most developers and replaced by synchronized. Synchronized is a heavy lock. If you have any concerns about performance High requirements, then under the same circumstances, variable declaration volatile will reduce less synchronization overhead.


Before introducing, let's ask 2 questions:

1. How exactly does volatile ensure the synchronization of shared variables?

2. Why is the i++ operation not an atomic operation for the virtual machine?

 

1. Principle of volatile read and write synchronization

 

After the variable is declared volatile, it will have the following characteristics:

1. Visibility. This variable is guaranteed to be visible to all threads.

2. Atomicity. Only reads/writes to any single volatile variable are atomic (not all).

3. Orderly. Variables declared volatile disable instruction reordering optimization

 

happen-before guaranteed visibility

The write-read of volatile variables enables communication between threads. happens-before is the memory visibility guarantee provided to us by the java memory model, which is the answer to our first question, how does volatiel guarantee synchronization of shared variables.

Let's first recall the happens-before principle (we only talk about it):

Program Order Law : If in a program, all actions A appear before action B, then every action A in a thread happens-before every action B in that thread.


Volatile variable rule : Writes to the Volatile field happen-before each subsequent read of the same Volatile.


Transitive : If A happens-before B, and B happens-before C, then A happens-before C.


We illustrate the application of these rules with an example:

public class VolatileTest {

    private int a =0;

    private volatile int b=0;


    public void write(){

        a = 1;          //1

        b = 2;          //2

    }

    public void read(){

        int i = b;          //3

        int j = a;          //4

    }

}

For example, there are now threads A and B, which call the write and read methods respectively .

 

The first case:

After thread A executes the write method first, thread B executes the read method. So:

1. Based on the program order rule. 1 happens-before 2; 3 happens-before 4

2. Based on the volatile principle. 2 happens-before 3;

3. Based on the principle of transitivity. Because 1 happens-before 2, 2 happens-before 3, and 3 happens-before 4. Then it can be deduced that 1 happens-before 4, and 2 happens-before 4.

In this case, we can determine that the values ​​of a and b written in thread A can be read in thread B at this time. (a value can still be read without declaring volatile, why we will talk about this later)

 

Second case:

Thread B executes the read method first, and then thread A executes the write method.

1. Based on the program order rule. 3 happens-before 4; 1 happens-before 2

2. Based on the volatile principle. without;

3. Based on the principle of transitivity. no delivery;

In this case, we can determine that the values ​​of a and b written in thread A are not read in thread B at this time.

 

Through the above analysis, we can define the volatile variable as follows:

  •  When writing a volatile variable, JMM will flush the shared variable in the local memory corresponding to the thread to the main memory.
  •  When reading a volatile variable, the JMM invalidates the thread's corresponding local memory. The thread will next read the shared variable from main memory.


For the first case, let's see how the above example writes and reads:

So reading this, there is a confusion: the above variable a is not declared as volatile, why can it be flushed to the main memory, and won't it be reordered by the processor?

 

Second, volatile restricted reordering

 

In the above, we mentioned that there is a feature in volatile, ordering, which prevents jvm from reordering it, so let's take a look at how it is done.

Reordering is divided into compiler reordering and processor reordering. In order to implement volatile memory semantics, the JVM restricts these two types of reordering types separately.


Compiler reordering

The volatile reordering rules for compilers:

first action

second operation

 

Normal read/write

volatile read

volatile write

Normal read/write

 

 

NO

volatile read

NO

NO

NO

volatile write

 

NO

NO

In the above table, NO means that the jvm cannot be reordered, keeping the current order .

For example, the first row and the third column indicate: the first operation is a normal read and write operation of a variable, and the second operation is a variable write operation declared by volatile, then operation 1 and operation 2 cannot be reordered at this time, keep current order.

Just like the a and b variables in the above example, to meet this situation, the order of operations of a and b remains unchanged.

The above rules are described in words:

  • When the second operation is a volatile write, no reordering is possible regardless of what the first operation was. This rule ensures that operations before a volatile write are not reordered by the compiler after a volatile write.
  • When the first operation is a volatile read, no reordering is possible regardless of the second operation. This rule ensures that operations after a volatile read are not reordered by the compiler before a volatile read.
  • When the first operation is a volatile write and the second operation is a volatile read, reordering is not possible.


Note that the jvm only guarantees that 2 operations keep this rule and cannot be extended to more than 2 operations .

 

processor reordering

In order to implement the above rules, the jvm compiler inserts memory barriers in the instruction sequence to prohibit certain types of processor reordering when generating bytecode.

Insert a StoreStore barrier in front of each volatile write operation.
Insert a StoreLoad barrier after each volatile write operation.
Inserts a LoadLoad barrier in front of each volatile read.
Insert a LoadStore barrier after each volatile read.

This ensures that the correct volatile reordering rules can be implemented in any program on any processor platform.


Summarize

volatile prevents reordering, what does it do?

Happens-before is the memory visibility guarantee provided to us by the java memory model; and volatile's prohibition of reordering rules, including volatile's compiler reordering rules and volatile's memory barrier insertion strategy, is the way JVM uses to implement happens-before.

For example, in the above program, according to the program sequence rules of happens-before: 1 happens-before 2; 3 happens-before 4.

Then according to the volatile rule: 2 happens-before 3. The sequence of operations 1, 2, 3, and 4 continues.

That is to say, volatile prohibits reordering rules to ensure the above happens-before order.

 

3. Application

i++ is not an atom

In the above principle introduction, we said that volatile is atomic only for the read/write of any single volatile variable. For example, the assignment operation of variable a can be atomic, but variable a++ is not atomic. Let's look at an example:

public class Test {

    private volatile  int count;

    public void increCount(){

        count++;
    }

    public void setCount(int count ){

        this.count=count;
    }

}

Let's use javap to look at the compilation instructions of incrementCount:

I look at the part circled in red, incrementCount is broken down into 4 instructions to operate, while setCount has only 1 instruction to process (atomic). In our code, the incrementCount method can be equivalent to the following:

    public void increCount(){

//        count++;
        int tmp =getCount();
        tmp=tmp+1;
        setCount(tmp);
    }

Therefore, volatile is only atomic for the read/write of any single volatile variable, while i++ is actually a combined operation consisting of a sequence of read-modify-write operations, which belong to multiple operations, so it does not have atomicity sex.

 

Volatile application principles

For a volatile variable to provide ideal thread safety, both of the following conditions must be met:

  • Writes to variables do not depend on the current value.
  • The variable is not contained in an invariant with other variables.


That is, these valid values ​​written to volatile variables are independent of any program state, including the current state of the variable.

So use volatile only if the state is truly independent of the rest of the program - a rule that avoids extending these patterns to unsafe use cases.

 

Application example

 

1. Assignment operation

The above increCount belongs to the application that depends on the current count value, while setCount does not depend on the current value. So the latter is thread safe.

 

2. Thread cancellation

When a thread is canceled or interrupted, someone will use the interrupted method to interrupt. If a volatile variable is maintained, no matter how the external thread calls it, immediate visibility to the current thread can always be guaranteed.

public class  CancleThread implements Runnable{

    private volatile  boolean cancle = false;

    public void shutdown(){

        this.cancle=true;
    }

   

    public void run() {

        while (!cancle){

            //.....doSomeThing
        }

    }

}

When you want to terminate the operation of this thread, it is safer to call the shutdown method.

 

Through the above principles and application introduction, you must not be so unfamiliar with volatile. Mastering the principles and understanding the boundaries of use will make your programs perform better and more readable. If we strictly follow the conditions for the use of volatile - that a variable is truly independent of other variables and its own previous value - you can use volatile instead of synchronized to simplify the code in some cases.

 

-----------------------------------------------------------------------------

If you want to see more interesting and original technical articles, scan and follow the official account.

Focus on personal growth and game development, and promote the growth and progress of the domestic game community.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324455601&siteId=291194637