Volatile底层实现原理

在java中,关键字volatile那是必须要掌握的,这在多线程并发中大量被使用。从之前的jdk源码也可以知道,volatile和CAS构成了java语言高并发的基石。我们一般会把volatile称为轻量级的锁,有时我们在使用volatile的时候能够达到更高的并发。那么关键字 volatile的作用是什么?我们在实际中如果需要使用volatile的话,无非是保证有序性和可见性。有序性和可见性在之前的 并发的特性中有介绍。那么volatile是怎么实现这两个特性的呢。

 

可见性

在了解可见性性之前,我们得熟悉几个cpu的指令编码,这样能方便我们快速理解这块的实现(图片源自java并发编程的艺术)。

 

Volatile底层实现原理_第1张图片

 

如果我们对一个变量加上volatile关键字,那么在编译成汇编的时候会加上lock前缀,那么这个lock前缀就是关键能完成这个可见性的操作,具体实现如下:

1)将当前处理器的缓存行数据写回到系统内存

2)这个写回内存的操作会使其他cpu里面缓存的了该内存地址的变为无效

这两个和我们之前讲的内存模型里面是一致的,就是实现写回主存,其他内存地址无效,那么其他地址如果要读取数据的话,就必须要从主存中从新拉取数据。

 

有序性

指令重排序是java为了提升性能而对指令进行重新排序。指令重排序包括以下几个重排序过程:编译器重排序 --》指令集并行重排序 --》内存系统的重排序。实际上就是从代码到cpu执行的一系列过程,为了优化性能,都进行了相应的指令重排序的操作。但是在单线程的情况下,这些指令的重排序是能够保证执行的结果和实际看到代码的顺序的结果是一样的,但是在多线程的情况下 ,指令重排序就可能导致意外的“惊喜”。

volatile在实现上是通过限制编译器重排序,指令集重排序实现的。Volatile关键字规定了编译器的重排序规则:(图片来自java并发编程的艺术)

 

Volatile底层实现原理_第2张图片

 

从这个表格上我们可以看出以下几点:

1) 第二个操作是volatile写时,第一个操作无论是啥都不能重排序这个操作保证写前和写后的不会顺序错乱,写前的不会在写后操作。

2) 第一个操作是volatile读时,第二个操作无论是啥都不能重排序。这个保证volatile读的顺序,读后的不会到读前面。

3) 第一个操作是volatile写,第二个操作是volatile读时,不能重排序这个保证volatile写在读之前。

 

Ok,当我们了解了这几条规则之后,我们JMM是怎么实现这些规则的,通过一种叫做内存屏障的保护进行,这个之前在unsafe类中也有提到loadFence,storeFence等类似内存屏障的操作。JMM在使用内存屏障采用的策略如下:

1)在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,用来完成对写的保护。 

2)在每个volatile读操作的后面插入一个LoadLoad屏障和LoadStore屏障,用来完成对读操作的保护。

 

下面我们看一个例子:

1. public class Test{  

2.     volatile int v1 = 1;   

3.     public void testVolatile() {   

4.         int i = v1; // 第一个volatile读    

5.         v1 = i + 1; // 第一个volatile写    

6.         //...   

7.     }  

8. }  

那么编译器生成的字节码将会如下:

 Volatile底层实现原理_第3张图片

上面是编译器处理后的指令,但是有些cpu产商会把这些屏障在进一步优化,但是这个优化实际上是能够保证先后顺序的。

虽然现在当我们了解了volatile的实现原理,但是实际上我们如果想要用volatile替换锁的话,还是要慎重考虑,因为多线程在实际生产中真的有可能 会导致很诡异的情况,这种往往要定位很久。但是如果项目对性能要求不是特别高的话,使用锁是一种比较保险且有效的方法实现多线程的同步的。

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

                                                                   Volatile底层实现原理_第4张图片

你可能感兴趣的:(java多线程,多线程,java)