Java进阶——volatile + JMM + 双重检查创建单例对象的时候为什么要加volatile关键字?!!
Posted 高、远
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java进阶——volatile + JMM + 双重检查创建单例对象的时候为什么要加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)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
【4】doublecheck创建单例为什么需要加volatile?
public class SingleInstance{
private volatile static SingleInstance instance;
public static TestInstance getInstance(){ //a
if(instance == null){ //b
synchronized(TestInstance.class){ //c
if(instance == null){ //d
instance = new TestInstance(); //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关键字?!!的主要内容,如果未能解决你的问题,请参考以下文章