Java进阶—— volatile JMM双重检查以及创建单例对象的时候为什么要加volatile关键字?
Posted 高、远
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java进阶—— volatile JMM双重检查以及创建单例对象的时候为什么要加volatile关键字?相关的知识,希望对你有一定的参考价值。
文章目录
- 【1】volatile的原理是什么?
- 【2】关于JMM(清楚volatile的前提)
- 【3】volatile变量的特性
- 【3】volatile为什么不能保证线程安全
- 【4】doublecheck创建单例为什么需要加volatile?
【1】volatile的原理是什么?
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将对缓存的修改操作立即写入主存;
如果是写操作,它会导致其他CPU中对应的缓存行无效。
【2】关于JMM(清楚volatile的前提)
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
这儿特别提一下:
特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。
详细了解更多JMM知识:https://www.jianshu.com/p/8420ade6ff76
【3】volatile变量的特性
-
保证可见性,不保证原子性
(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;(2)这个写会操作会导致其他线程中的volatile变量缓存无效。
-
禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
(1)重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
【3】volatile为什么不能保证线程安全
多的话我就不说了,他不具备原子性。当一个线程已经取了这个数据到对应的缓存中,并且已经从自己的缓存中获取到这个值了,这个时候这个线程挂起,其他线程来执行更改变量的值,但是前面的那个线程已经不需要从自己的缓存中取数据了(缓存中的数据已经变了),这个时候前面的线程把数据写到缓存中,并且更新到主线程,问题就出现了:
我们来假设一个场景,两个线程需要对a=1的值进行修改,线程1拿到缓存行中a的值,但是还没得及修改,线程2也已经取到另一个处理器中缓存行中的值,并且刷新到主存当中,a=2,此时其他处理器中缓存行的数据失效,这时线程1也将修改的a的值a=2写到缓存中,并且写会主存,因此其他缓存行失效,这时就出现了线程安全,数据不一致的问题,两个线程都对a进行加1修改操作,但是a=2,它的值只相加了1。这样看来,volatile关键字修改的变量即使在缓存行中也不具有原子性了。
【4】doublecheck创建单例为什么需要加volatile?
public class SingleInstance
private volatile static SingleInstance instance;
public static SingleInstance getInstance() //a
if(instance == null) //b
synchronized(SingleInstance.class) //c
if(instance == null) //d
instance = new SingleInstance(); //e
return instance; //f
- 在这里需要知道JVM是如何创建对象的,来看下图:
所以正常情况下我们是按照
1——>2——>3——>4——>5
来执行的,但是如果考虑指令重排序。有可能5
在3
之前执行,也就是先让引用指向这片空间
,再让这篇空间设置零值
。在多线程的情况下会出现以下问题。当线程A在执行第e
行代码时,B线程进来执行到第b
行代码。假设此时A执行的过程中发生了指令重排序,即先执行了1、2、和5
,没有执行2、3
。那么由于A线程执行了5
导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第f
句话并返回一个未初始化的对象。
以上是关于Java进阶—— volatile JMM双重检查以及创建单例对象的时候为什么要加volatile关键字?的主要内容,如果未能解决你的问题,请参考以下文章