DCL单例需要加volatile关键字吗?

目录

  • 什么是DCL单例?
  • 对象初始化的过程
    • 解析Java代码的反汇编指令
  • CPU指令重排序
  • volatile关键字的语义
  • 最终结论

什么是DCL单例?

实现单例模式的方式有很多种,如:饿汉式、懒汉式、枚举等。
DCL(Double Check Lock)双重检查加锁,就是懒汉式的一种实现方式,代码实现如下:
DCL单例需要加volatile关键字吗?_第1张图片
开启多线程去获取对象,确实T的实例在堆中只会存在一个,单例是可行的,测试代码如下:
DCL单例需要加volatile关键字吗?_第2张图片
DCL的方式确实可以实现单例,但它是有缺陷的:线程获取到的对象可能未被初始化

对象初始化的过程

Person person = new Person();

如上代码,当使用new关键字创建一个对象时,JVM需要做哪些事情?

  1. 为对象分配内存
    • 优先栈上分配
    • 栈中不能分配的,对象是否足够大?大的直接分配进老年代。
    • TLAB中是否可以快分配?
    • 不能则在Eden区慢分配。
  2. 内存分配完毕,属性设置默认值(引用类型为null,基本类型为对应的默认值)。
  3. 执行构造函数,属性设置初始值。
  4. 建立连接,引用指向对象内存地址。

解析Java代码的反汇编指令

DCL单例需要加volatile关键字吗?_第3张图片
如上代码,当我们创建一个Person实例,并将其person指向其引用时,JVM需要执行哪些指令呢?
javac Person.java得到字节码文件,再javap -c Person.class即可拿到反汇编指令,如下:
DCL单例需要加volatile关键字吗?_第4张图片
可以看到,new一个对象,虽然只有一行代码,实际上需要经过好几个过程,而且这些过程并非顺序执行。
有可能一个对象在未初始化时,就先建立连接了,一旦发生这种情况,使用DCL实现的单例模式,就会导致线程拿到的是一个未被初始化的对象。
想想看,未被初始化的对象,属性为引用类型则值全部为null,一旦对这些属性进行了操作,则会抛出空指针异常

为什么创建实例的过程未必顺序执行?

CPU指令重排序

现代CPU的算力已经十分优秀,大多数家用计算机的CPU运算速度为每秒50亿次。
相比之下,内存的读写速度就显得十分缓慢。
为了跨越两者速度上的鸿沟,CPU不得不进行一些优化,指令重排序就是其中之一。

例如:指令A需要从内存中读写数据,指令B不需要,只是进行简单的运算,且指令A和指令B之间没有依赖关系,那么这时候CPU就会进行指令重排序,无需等待指令A执行完毕,指令B就可以执行或先执行。

如下代码,期望spin()在1秒后结束,但遗憾的是,程序永远不会停止
DCL单例需要加volatile关键字吗?_第5张图片
这就是指令重排序,导致程序运行结果和期望不一致。

volatile关键字的语义

  • 多线程间的可见性
  • 禁止指令重排序

使用volatile修饰的变量,可以保证多线程间的可见性,当线程对其进行读取时,JVM会强制要求线程从主存中去读,写入时,也会要求线程强制写入到主存,以保证其他线程读取的是最新值。

使用volatile修饰的变量会禁止指令重排序,即保证建立连接前,对象一定被初始化过了,不存在读取到的是一个未被初始化的一个半初始化状态的对象。

最终结论

DCL实现单例模式,是需要加volatile关键字修饰单例的,否则可能导致线程读取到的对象是一个未被初始化的对象,导致程序出现不可预知的错误。
虽然这个现象并不好模拟,很可能不加你的程序也能很好的运行很多年而不出错,但是一旦出现这种状况,就会让人摸不着头脑。
所以,为了让程序更加的健壮,还是建议大家在使用DCL的时候加上volatile关键字。

你可能感兴趣的:(Java)