DCL 单例模式是否需要volatile?

1. volatile的作用

        volatile只能用来修饰成员变量,它有两大特性:可见性、有序性,此处的有序性区别于synchornized的有序性。synchornized的有序性指的是多个线程要有序地执行临界区的代码,而volatile的有序性指的是指令有序性(指令不可重排)。
什么是指令重排序?
        指令重排序是指在运行程序时,处理器为了优化性能,代码的执行顺序可能不会按代码的顺序执行,比如前后两个赋值指令,第一条需要去磁盘读取数据,第二条需要去内存读取数据,程序在执行的时候可能会在去磁盘读取数据的时候先把第二条赋值指令执行完,等从磁盘读完数据再执行第一条赋值指令,这就叫指令重拍序,而volatile保证的有序性即防止指令重排。
指令重排发生的条件?
        不影响单线程的最终一致性,也就是说,在单个线程内,两条指令变换顺序,不会影响最终结果,那么处理器可能会因为性能考虑指令重排,比如两条单纯赋值的指令

2. DCL是否需要volatile关键字修饰?

        先说一下结论:必须要volatile修饰,否则可能会获取到半初始化对象从而引发程序未知错误。下面分析。
        DCL全称Double Check Lock,双重检测锁,是单例模式中非常经典的并发案例,代码如下:

public class Singleton {
	public int num = 6;
	private static /*volatile*/ Singleton singleton;
	private Singleton() {}
	public static Singleton getInstance() {
		if (singleton == null) {
			synchornized (Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

        在分析singleton是否需要被volatile修饰之前,我们先搞清楚new Singleton()底层指令到底是怎样的,然后再一步步分析
底层汇编指令如下:

 0 new #2 <Singleton>	// new 分配空间, 用默认值清理分配的空间, 即给num赋默认值0
 3 dup // 暂且不说, 与本文无关且比较复杂
 4 invokespecial #3 <Singleton.<init>>	// 调用构造, 调用完了num的值才会为6. 构造过程是给成员变量赋初始值的过程
 7 astore_1	// 建立singleton与对象的关联, 在此之前 singleton == null
 8 return	// 返回、结束

        下面说一个场景,前提是不加volatile。假设现在有2个线程同时调用了getInstance(),假设是第一次调用,a线程开始执行,第一次判空为true,然后a线程获取锁,进入临界区,a线程第二次判空为true,开始创建对象,执行汇编,注意,此时发生了指令重排,创建对象的汇编指令被重排为如下顺序

 0 new #2 <Singleton>
 3 dup
 4 astore_1
 7 invokespecial #3 <Singleton.<init>>
 8 return

        可以看到指令变为先建立引用与对象之间的连接,再初始化对象,一旦建立了连接,引用便不为null,假设a线程执行到创建对象的汇编指令的astore_1指令,b线程开始调用getInstance()方法,此时singleton不为null,b线程第一次判空为false,直接返回半初始化对象,可能就会出现错误,由此可以得出DCL单例模式必须使用volatile修饰,否则可能会出问题。
        能力有限,不足之处欢迎指正~

你可能感兴趣的:(Java面试总结,java技术分享,单例模式,java,开发语言)