Home >Java >javaTutorial >Analyze the sample code of Java volatile keyword implementation from the root (picture)

Analyze the sample code of Java volatile keyword implementation from the root (picture)

黄舟
黄舟Original
2017-03-22 10:47:231428browse

1. AnalysisOverview

  1. Related concepts of memory model

  2. Three concepts in concurrent programming

  3. Java memory model

  4. In-depth analysisVolatileKeywords

  5. Scenarios for using the volatile keyword

2. Memory model related Concept

Cache consistency problem. Variables that are accessed by multiple threads are usually called shared variables.

That is, if a variable is cached in multiple CPUs (generally). Only occurs in multi-threaded programming), then there may be cache inconsistency problems

In order to solve the cache inconsistency problem, there are usually two solutions:

  • By adding LOCK# to the bus

  • Through the cache consistency protocol

These two methods are both at the hardware level The method provided above.

The above method 1 will have a problem because other CPUs cannot access the memory during the locking period, resulting in inefficiency.

The most famous cache consistency protocol. The MESI protocol is Intel's MESI protocol. The MESI protocol ensures that the copy of the shared variables used in each cache is consistent. Its core idea is: when the CPU writes data, if the operated variable is found to be a shared variable, it will be deleted on other CPUs. There is also a copy of the variable in the CPU, which will send a signal to notify other CPUs to invalidate the cache line of the variable. Therefore, when other CPUs need to read this variable, they find that the cache line caching the variable in their own cache is invalid. Then it will re-read from the memory

##3. Three concepts in concurrent programming

In concurrent programming, we usually encounter it. The following three issues: atomicity problem, visibility problem, ordering problem

3.1 Atomicity

Atomicity: that is, the process of one operation or multiple operations being executed in full. It will not be interrupted by any factors, or it will not be executed.

3.2 Visibility

Visibility means that when multiple threads access the same variable, one thread modifies the variable. value, other threads can immediately see the modified value.

3.3 Orderliness

Orderliness: That is, the order of program execution is executed in the order of code

. Judging from the code sequence, statement 1 is before statement 2, so when the JVM actually executes this code, will it ensure that statement 1 will be executed before statement 2? Not necessarily, why may instruction duplication occur here? Sorting (Instruction Reorder).

The following explains what instruction reordering is. Generally speaking, in order to improve the efficiency of program operation, the processor may optimize the input code. It does not guarantee that the execution order of each statement in the program is the same as in the code. The sequence is consistent, but it will ensure that the final execution result of the program is consistent with the result of the sequential execution of the code.

Instruction reordering will not affect the execution of a single thread, but will affect the correctness of concurrent execution of threads.

In other words, in order for concurrent programs to execute correctly, atomicity, visibility, and orderliness must be guaranteed. As long as one of them is not guaranteed, it may cause the program to run incorrectly.

4. Java Memory Model

In the Java virtual machine specification, we try to define a Java memory model (Java Memory

Model, JMM) to shield various hardware platforms and operations. System memory access differences to achieve consistent memory access effects for Java programs on various platforms. So what does the Java memory model stipulate? It defines the access rules for variables in the program. To a larger extent, it defines the order of program execution. Note that in order to obtain better execution performance, the Java memory model does not restrict the execution engine from using the processor's registers or cache to improve instruction execution speed, nor does it restrict the compiler from reordering instructions. In other words, in the Java memory model, there will also be cache consistency issues and instruction reordering issues.

The Java memory model stipulates that all variables are stored in main memory (similar to the physical memory mentioned earlier), and each thread has its own working memory (similar to the previous cache). All operations on variables by threads must be performed in working memory and cannot directly operate on main memory. And each thread cannot access the working memory of other threads.

4.1 Atomicity

In Java, reading and assigning operations to variables of basic

data types are atomic operations, that is, these operations cannot be interrupted. , either executed or not executed.

Please analyze which of the following operations are atomic operations:

  1. x = 10; //Statement 1

  2. y = x; //Statement 2

  3. x++; //Statement 3

  4. x = x + 1; //Statement 4

In fact, only statement 1 is an atomic operation, and the other three statements are not atomic operations.

In other words, only simple reading and assignment (and the number must be assigned to a variable, mutual assignment between variables is not an atomic operation) are atomic operations.

As can be seen from the above, the Java memory model only guarantees that basic reading and assignment are atomic operations. If you want to achieve atomicity for a larger range of operations, you can achieve it through synchronized and Lock.

4.2 Visibility

For visibility, Java provides the volatile keyword to ensure visibility.

When a shared variable is modified volatile, it will ensure that the modified value will be updated to the main memory immediately. When other threads need to read it, it will read the new value from the memory.

Ordinary shared variables cannot guarantee visibility, because after an ordinary shared variable is modified, it is uncertain when it will be written to the main memory. When other threads read it, the memory may still be The original old value, so visibility is not guaranteed.

In addition, visibility can also be guaranteed through synchronized and Lock. Synchronized and Lock can ensure that only one thread acquires the lock at the same time and then executes the synchronization code, and the modification of the variable will be refreshed to the main memory before releasing the lock. among. Visibility is therefore guaranteed.

4.3 Orderliness

In the Java memory model, the compiler and processor are allowed to reorder instructions, but the reordering process will not affect the execution of single-threaded programs, but it will Affects the correctness of multi-threaded concurrent execution.

In Java, you can use the volatile keyword to ensure a certain "orderliness" (it can prohibit instruction reordering). In addition, orderliness can be ensured through synchronized and Lock. Obviously, synchronized and Lock ensure that one thread executes synchronization code at each moment, which is equivalent to letting threads execute synchronization code sequentially, which naturally ensures orderliness.

In addition, the Java memory model has some innate "orderliness", that is, orderliness that can be guaranteed without any means. This is often called the happens-before principle. If the execution order of two operations cannot be deduced from the happens-before principle, then their ordering is not guaranteed, and the virtual machine can reorder them at will.

Let’s introduce the happens-before principle in detail:

  1. Program sequence rules: Within a thread, according to the code order, write in front The operation occurs first before the operation written later

  2. Locking rule: An unLock operation occurs first before the same lock operation occurs later

  3. volatileVariable rules: A write operation to a variable occurs first before a subsequent read operation to this variable

  4. Transmission rule: If operation A occurs first before operation B, and operation B occurs first before operation C, it can be concluded that operation A occurs first before operation C

  5. Thread startup rule: The start() method of the Thread object occurs first in this thread Every action

  6. Thread interruption rules: The call to the thread interrupt() method occurs first when the code of the interrupted thread detects the occurrence of the interrupt event

  7. Thread termination rules: All operations in a thread occur first when the thread is terminated. We can detect that the thread has terminated by ending the Thread.join() method and the return value of Thread.isAlive()

  8. Object termination rules: The initialization of an object occurs first at the beginning of its finalize() method

Among these 8 rules, the first The 4 rules are more important, and the last 4 rules are obvious.

Let’s explain the first 4 rules:

  1. For program sequence rules, my understanding is that the execution of a piece of program code looks like it in a single thread. is in order. Note that although this rule mentions that "operations written in the front occur first before operations written in the back", this should mean that the order in which the program appears to be executed is in the order of the code, because the virtual machine may perform changes to the program code. Instructions reordered. Although reordering is performed, the final execution result is consistent with the result of the program's sequential execution. It will only reorder instructions that do not have data dependencies. Therefore, in a single thread, program execution appears to be executed in order, which should be understood. In fact, this rule is used to ensure the correctness of the program execution results in a single thread, but it cannot guarantee the correctness of the program execution in multiple threads.

  2. The second rule is also easier to understand. That is to say, whether in single thread or multi-thread, if the same lock is in a locked state, the lock must be updated first. After the release operation is performed, the lock operation can be continued later.

  3. The third rule is a more important rule, and it is also the focus of the following article. The intuitive explanation is that if a thread writes a variable first, and then a thread reads it, then the write operation will definitely occur before the read operation.

  4. The fourth rule actually reflects the transitive nature of the happens-before principle.

5. In-depth analysis of the volatile keyword

5.1 Two-level semantics of the Volatile keyword

Once a shared variable (class member variable, class After the static member variable) is modified by volatile, it has two layers of semantics:

  1. ensures the visibility when different threads operate on this variable, that is, one thread modifies a certain The value of a variable, the new value is immediately visible to other threads.

  2. Instruction reordering is prohibited.

Regarding visibility, let’s look at a piece of code first. If thread 1 is executed first and thread 2 is executed later:

//线程1
boolean stop = false;
while(!stop){
doSomething();
}

//线程2
stop = true;

This code is a very typical piece of code, there are many People may use this marking method when interrupting a thread. But in fact, will this code run completely correctly? That is, will the thread be interrupted? Not necessarily, maybe most of the time, this code can interrupt the thread, but it may also cause the thread to be unable to be interrupted (although this possibility is very small, but once this happens, it will cause an infinite loop).

The following explains why this code may cause the thread to be unable to be interrupted. As explained before, each thread has its own working memory during running, so when thread 1 is running, it will copy the value of the stop variable and put it in its own working memory.

Then when thread 2 changes the value of the stop variable, but before it has time to write it into the main memory, thread 2 turns to do other things, then thread 1 does not know the change of the stop variable by thread 2 , so the cycle will continue.

But after using volatile modification, it becomes different:

  • First: using the volatile keyword will force the modified value to be written to the main memory immediately;

  • Second: If the volatile keyword is used, when thread 2 makes a modification, it will cause the cache line of the cache variable stop in the working memory of thread 1 to be invalid (if it is reflected to the hardware layer, that is The corresponding cache line in the CPU's L1 or L2 cache is invalid);

  • Third: Since the cache line of the cache variable stop in the working memory of thread 1 is invalid, thread 1 reads it again The value of the variable stop will be read from the main memory.

Then when thread 2 modifies the stop value (of course, this includes 2 operations, modifying the value in the working memory of thread 2, and then writing the modified value into the memory), it will cause The cache line of the cache variable stop in the working memory of thread 1 is invalid. Then when thread 1 reads it, it finds that its cache line is invalid. It will wait for the main memory address corresponding to the cache line to be updated, and then read from the corresponding main memory. latest value.

Then what thread 1 reads is the latest correct value.

5.2 Does volatile guarantee atomicity?

Volatile does not guarantee atomicity. Let’s look at an example below.

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

Everyone think about the output of this program? Maybe some friends think it is 10,000. But in fact, when you run it, you will find that the results are inconsistent every time, and they are always a number less than 10,000.

There is a misunderstanding here. The volatile keyword can ensure that visibility is correct, but the error in the above program is that it fails to guarantee atomicity. Visibility can only ensure that the latest value is read every time, but volatile cannot guarantee the atomicity of operations on variables.

As mentioned before, the auto-increment operation is not atomic. It includes reading the original value of the variable, adding 1, and writing to the working memory. That means that the three sub-operations of the auto-increment operation may be executed separately, which may lead to the following situation:

If the value of the variable inc is 10 at a certain moment.

Thread 1 performs an auto-increment operation on the variable. Thread 1 first reads the original value of the variable inc, and then thread 1 is blocked;

Then thread 2 performs an auto-increment operation on the variable. Thread 2 also reads the original value of the variable inc. Since thread 1 only reads the variable inc and does not modify the variable, the cache line of the cached variable inc in the working memory of thread 2 will not be invalid. Therefore, thread 2 will directly go to the main memory to read the value of inc, find that the value of inc is 10, then add 1, and write 11 to the working memory, and finally write it to the main memory.

Then thread 1 then adds 1. Since the value of inc has been read, note that the value of inc in the working memory of thread 1 is still 10 at this time, so thread 1 adds 1 to inc. The value of the last inc is 11, then 11 is written into the working memory, and finally into the main memory.

Then after the two threads perform an auto-increment operation respectively, inc only increases by 1.

After explaining this, some friends may have questions. No, isn’t it guaranteed that when a variable modifies a volatile variable, the cache line will be invalid? Then other threads will read the new value, yes, this is correct. This is the volatile variable rule in the happens-before rule above, but it should be noted that after thread 1 reads the variable and is blocked, the inc value is not modified. Then although volatile can ensure that thread 2 reads the value of variable inc from memory, thread 1 does not modify it, so thread 2 will not see the modified value at all.

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

把上面的代码改成以下任何一种都可以达到效果:

采用synchronized:

public class Test {
    public  int inc = 0;

    public synchronized void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用Lock:

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();

    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

采用AtomicInteger:

public class Test {
    public  AtomicInteger inc = new AtomicInteger();

    public  void increase() {
        inc.getAndIncrement();
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

5.3 volatile能保证有序性吗?

volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

举个例子:

//x、y为非volatile变量
//flag为volatile变量

x = 2;         //语句1
y = 0;         //语句2
flag = true;   //语句3
x = 4;         //语句4
y = -1;        //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

5.4 volatile的原理和实现机制

这里探讨一下volatile到底如何保证可见性和禁止指令重排序的。

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

6、使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  1. 对变量的写操作不依赖于当前值(比如++操作,上面有例子)

  2. 该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

下面列举几个Java中使用volatile的几个场景。

状态标记量

volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

double check

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;
    }
}

至于为何需要这么写请参考:


The above is the detailed content of Analyze the sample code of Java volatile keyword implementation from the root (picture). For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn