欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

什么是DCL单例模式(volatile关键字的使用)

程序员文章站 2022-03-22 10:11:43
目录什么是DCL单例?对象初始化的过程解析Java代码的反汇编指令CPU指令重排序volatile关键字的语义最终结论什么是DCL单例?实现单例模式的方式有很多种,如:饿汉式、懒汉式、枚举等。DCL(Double Check Lock)双重检查加锁,就是懒汉式的一种实现方式,代码实现如下:开启多线程去获取对象,确实T的实例在堆中只会存在一个,单例是可行的,测试代码如下:DCL的方式确实可以实现单例,但它是有缺陷的:线程获取到的对象可能未被初始化。对象初始化的过程Person person...



什么是DCL单例?

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

对象初始化的过程

Person person = new Person(); 

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

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

解析Java代码的反汇编指令

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

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

CPU指令重排序

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

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

如下代码,期望spin()在1秒后结束,但遗憾的是,程序永远不会停止
什么是DCL单例模式(volatile关键字的使用)
这就是指令重排序,导致程序运行结果和期望不一致。

volatile关键字的语义

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

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

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

最终结论

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

本文地址:https://blog.csdn.net/qq_32099833/article/details/107899270

相关标签: Java 多线程