Home  >  Article  >  Java  >  Detailed explanation of the use of volatile and analysis of its principles

Detailed explanation of the use of volatile and analysis of its principles

零下一度
零下一度Original
2017-06-25 11:04:481843browse

1. The role of volatile

In the article "Java Concurrent Programming: Core Theory", we have already mentioned the issues of visibility, ordering and atomicity. Usually We can solve these problems through the Synchronized keyword. However, if you understand the principle of Synchronized, you should know that Synchronized is a relatively heavyweight operation and has a relatively large impact on the performance of the system. Therefore, if there are other solutions , we usually avoid using Synchronized to solve problems. The volatile keyword is another solution provided in Java to solve the problem of visibility and ordering. Regarding atomicity, one thing needs to be emphasized, and it is also a point that is easy to misunderstand: a single read/write operation on volatile variables can guarantee atomicity, such as long and double type variables, but it does not guarantee the atomicity of operations such as i++. Because essentially i++ is a two-time operation of reading and writing.

2. The use of volatile

Regarding the use of volatile, we can illustrate its usage and scenarios through several examples.

1. Prevent reordering

We analyze the reordering problem from the most classic example. Everyone should be familiar with the implementation of the singleton pattern. To implement a singleton in a concurrent environment, we can usually use double check locking (DCL). The source code is as follows:

Detailed explanation of the use of volatile and analysis of its principles
 1 package com.paddx.test.concurrent;
 2 
 3 public class Singleton {
 4     public static volatile Singleton singleton;
 5 
 6     /**
 7      * 构造函数私有,禁止外部实例化
 8      */
 9     private Singleton() {};
10 
11     public static Singleton getInstance() {
12         if (singleton == null) {
13             synchronized (singleton) {
14                 if (singleton == null) {
15                     singleton = new Singleton();
16                 }
17             }
18         }
19         return singleton;
20     }
21 }
Detailed explanation of the use of volatile and analysis of its principles
## Now we analyze Let’s look at why we need to add the volatile keyword between variables singleton. To understand this problem, you must first understand the object construction process. Instantiating an object can actually be divided into three steps:

  (1) Allocate memory space.

  (2) Initialize the object.

  (3) Assign the address of the memory space to the corresponding reference.

But since the operating system can reorder instructions, the above process may also become the following process:

  (1) Allocate memory space.

  (2) Assign the address of the memory space to the corresponding reference.

 (3) Initializing the object

If this is the process, an uninitialized object reference may be exposed in a multi-threaded environment, leading to unpredictable results. Therefore, in order to prevent reordering of this process, we need to set the variable to a volatile type variable.

2. Implement visibility

The visibility problem mainly means that one thread modifies the shared variable value, but another thread cannot see it. . The main reason for the visibility problem is that each thread has its own cache area-thread working memory. The volatile keyword can effectively solve this problem. Let's look at the following example to know its effect:

Detailed explanation of the use of volatile and analysis of its principles
 1 package com.paddx.test.concurrent;
 2 
 3 public class VolatileTest {
 4     int a = 1;
 5     int b = 2;
 6 
 7     public void change(){
 8         a = 3;
 9         b = a;
10     }
11 
12     public void print(){
13         System.out.println("b="+b+";a="+a);
14     }
15 
16     public static void main(String[] args) {
17         while (true){
18             final VolatileTest test = new VolatileTest();
19             new Thread(new Runnable() {
20                 @Override
21                 public void run() {
22                     try {
23                         Thread.sleep(10);
24                     } catch (InterruptedException e) {
25                         e.printStackTrace();
26                     }
27                     test.change();
28                 }
29             }).start();
30 
31             new Thread(new Runnable() {
32                 @Override
33                 public void run() {
34                     try {
35                         Thread.sleep(10);
36                     } catch (InterruptedException e) {
37                         e.printStackTrace();
38                     }
39                     test.print();
40                 }
41             }).start();
42 
43         }
44     }
45 }
Detailed explanation of the use of volatile and analysis of its principles

  直观上说,这段代码的结果只可能有两种:b=3;a=3 或 b=2;a=1。不过运行上面的代码(可能时间上要长一点),你会发现除了上两种结果之外,还出现了第三种结果:

...... 
b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3
......

  为什么会出现b=3;a=1这种结果呢?正常情况下,如果先执行change方法,再执行print方法,输出结果应该为b=3;a=3。相反,如果先执行的print方法,再执行change方法,结果应该是 b=2;a=1。那b=3;a=1的结果是怎么出来的?原因就是第一个线程将值a=3修改后,但是对第二个线程是不可见的,所以才出现这一结果。如果将a和b都改成volatile类型的变量再执行,则再也不会出现b=3;a=1的结果了。

3、保证原子性

   关于原子性的问题,上面已经解释过。volatile只能保证对单次读/写的原子性。这个问题可以看下JLS中的描述:

17.7 Non-Atomic Treatment of double and long

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.

Writes and reads of volatile long and double values are always atomic.

Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.

Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.

Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.

  这段话的内容跟我前面的描述内容大致类似。因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。

  关于volatile变量对原子性保证,有一个问题容易被误解。现在我们就通过下列程序来演示一下这个问题:

Detailed explanation of the use of volatile and analysis of its principles
 1 package com.paddx.test.concurrent;
 2 
 3 public class VolatileTest01 {
 4     volatile int i;
 5 
 6     public void addI(){
 7         i++;
 8     }
 9 
10     public static void main(String[] args) throws InterruptedException {
11         final  VolatileTest01 test01 = new VolatileTest01();
12         for (int n = 0; n <div class="cnblogs_code_toolbar"><span class="cnblogs_code_copy"><img src="https://img.php.cn/upload/article/000/000/001/0313942818f37f12b8713dd37831b872-5.gif" alt="Detailed explanation of the use of volatile and analysis of its principles"></span></div>

You may mistakenly think that after adding the keyword volatile to variable i, this program is thread-safe. You can try running the above program. The following is the result of my local operation:

The result of everyone's operation may be different. However, it should be seen that volatile cannot guarantee atomicity (otherwise the result should be 1000). The reason is also very simple. i++ is actually a compound operation, including three steps:

 (1) Read the value of i.

 (2) Add 1 to i.

 (3) Write the value of i back to the memory.

Volatile cannot guarantee that these three operations are atomic. We can use AtomicInteger or Synchronized to ensure the atomicity of the +1 operation.

Note: The Thread.sleep() method is executed in many places in the above code. The purpose is to increase the probability of concurrency problems and has no other effect.

3. The principle of volatile

Through the above examples, we should basically know what volatile is and how to use it. Now let's take a look at how the underlying layer of volatile is implemented.

 1. Visibility implementation:

As mentioned in the previous article, the thread itself does not directly interact with the main memory, but through the thread's working memory to complete the corresponding operation. This is also the essential reason why data between threads is invisible. Therefore, to achieve the visibility of volatile variables, just start from this aspect. There are two main differences between writing operations to volatile variables and ordinary variables:

(1) When a volatile variable is modified, the modified value will be forced to be refreshed in the main memory.

 (2) Modifying volatile variables will cause the corresponding variable values ​​in the working memory of other threads to become invalid. Therefore, when you read the variable value, you need to read the value in the main memory again.

Through these two operations, the visibility problem of volatile variables can be solved.

  2. Orderly implementation:

Before explaining this problem, let’s first understand the happens-before rules in Java. The Happen-before rule in JSR 133 The definition of before is as follows:

Two actions can be ordered by a happens-before relationship. If one action happens before another, then the first is visible to and ordered before the second.

It’s more popular That is to say, if a happens-before b, then any operation done by a is visible to b. (Everyone must remember this, because the word "happen-before" is easily misunderstood as meaning before and after time). Let's take a look at what happen-before rules are defined in JSR 133:

• Each action in a thread happens before every subsequent action in that thread.
• An unlock on a monitor happens before every subsequent lock on that monitor.
• A write to a volatile field happens before every subsequent read of that volatile.
• A call to start() on a thread happens before any actions in the started thread.
• All actions in a thread happen before any other thread successfully returns from a join() on that thread.
• If an action a happens before an action b, and b happens before an action c, then a happens before c.

Translated as:

  • In the same thread, the previous operation happens-before the subsequent operation. (That is, the code is executed in order within a single thread. However, the compiler and processor can reorder without affecting the execution results in a single-threaded environment, which is legal. In other words, this rule cannot guarantee Compilation rearrangement and instruction rearrangement).

  • The unlocking operation on the monitor happens-before its subsequent locking operation. (Synchronized rules)

  • Writing operations on volatile variables happen-before subsequent read operations. (volatile rule)

  • The start() method of the thread happens-before all subsequent operations of the thread. (Thread startup rules)

  • All operations of a thread happen-before other threads call join on this thread and return successfully.

  • If a happens-before b, b happens-before c, then a happens-before c (transitive).

Here we mainly look at the third rule: the rules for ensuring orderliness of volatile variables. The article "Java Concurrent Programming: Core Theory" mentioned that reordering is divided into compiler reordering and processor reordering. In order to achieve volatile memory semantics, JMM will restrict these two types of reordering of volatile variables. The following is a table of reordering rules specified by JMM for volatile variables:

Can Reorder 2nd operation
1st operation Normal Load
Normal Store
Volatile Load Volatile Store
Normal Load
Normal Store
    No
Volatile Load No No No
Volatile store   No No

  3、内存屏障

  为了实现volatile可见性和happen-befor的语义。JVM底层是通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。下面是完成上述规则所要求的内存屏障:

Required barriers 2nd operation
1st operation Normal Load Normal Store Volatile Load Volatile Store
Normal Load       LoadStore
Normal Store       StoreStore
Volatile Load LoadLoad LoadStore LoadLoad LoadStore
Volatile Store     StoreLoad StoreStore

(1)LoadLoad 屏障
执行顺序:Load1—>Loadload—>Load2
确保Load2及后续Load指令加载数据之前能访问到Load1加载的数据。

(2)StoreStore 屏障
执行顺序:Store1—>StoreStore—>Store2
确保Store2以及后续Store指令执行前,Store1操作的数据对其它处理器可见。

(3)LoadStore 屏障
执行顺序: Load1—>LoadStore—>Store2
确保Store2和后续Store指令执行前,可以访问到Load1加载的数据。

(4)StoreLoad 屏障
执行顺序: Store1—> StoreLoad—>Load2
确保Load2和后续的Load指令读取之前,Store1的数据对其他处理器是可见的。

最后我可以通过一个实例来说明一下JVM中是如何插入内存屏障的:

Detailed explanation of the use of volatile and analysis of its principles
 1 package com.paddx.test.concurrent;
 2 
 3 public class MemoryBarrier {
 4     int a, b;
 5     volatile int v, u;
 6 
 7     void f() {
 8         int i, j;
 9 
10         i = a;
11         j = b;
12         i = v;
13         //LoadLoad
14         j = u;
15         //LoadStore
16         a = i;
17         b = j;
18         //StoreStore
19         v = i;
20         //StoreStore
21         u = j;
22         //StoreLoad
23         i = u;
24         //LoadLoad
25         //LoadStore
26         j = b;
27         a = i;
28     }
29 }
Detailed explanation of the use of volatile and analysis of its principles

四、总结

  总体上来说volatile的理解还是比较困难的,如果不是特别理解,也不用急,完全理解需要一个过程,在后续的文章中也还会多次看到volatile的使用场景。这里暂且对volatile的基础知识和原来有一个基本的了解。总体来说,volatile是并发编程中的一种优化,在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

  (1)对变量的写操作不依赖于当前值。

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

The above is the detailed content of Detailed explanation of the use of volatile and analysis of its principles. 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