DCL单例到底需不需要volatile?
时间:2023-06-29 03:37:00
DCL需要单例吗?volatile?
-
- 介绍前言知识
-
- 对象的创建过程
- 半初始化
- DCL(Double Check Lock)
-
- volatile关键字
- 指令重排序
- 最后
介绍前言知识
首先,让我们来看看下面的程序。这个程序很简单。这个程序的运行结果是什么?8还是0?
public class Escape {
private int num = 8; private Escape() {
new Thread(() -> System.out.println(this.num)).start(); } public static void main(String[] args) throws IOException {
new Escape(); System.in.read(); } }
对象的创建过程
java在程序运行过程中,每时每刻都有对象被创建,在语言层面上,创建对象只是一个new只是关键词,字节码层面的对象创建过程是什么?
LINENUMBER 21 L0 NEW lixinzhen/Escape DUP INVOKESPECIAL lixinzhen/Escape.<init> ()V POP
这是前面程序反编译的字节码。由此可见,创建对象需要经过三个步骤。第一步是申请内存,给对象默认值0(此时已成为半初始化状态)。第二步是调用构造方法初始化。对象赋值为8,第三步是建立连接(执行到INVOKESPECIAL这里的对象已经创建)。(DUP这个指令涉及到堆栈的调用,这里不介绍太复杂了)
半初始化
在创建对象的过程中,作者提到了半初始状态。你有没有想过为什么要有半初始状态,也就是说,为什么要给它?num对象赋予初始值0?原因在于给num对象分配的内存区域以前可能有一定的值,以防止num该值被错误分配,因此首先给出初始值。(这也是为什么Java比C/C 更安全的原因,早期c/c 指针暴露在外面,可以随时窃取密码)
DCL(Double Check Lock)
在设计模式中,最简单的是单例模式,那么什么是单例模式呢?我相信我们都知道,所谓的单例模式是在整个类别的运行过程中,只有一个对象的例子。作者还介绍了单例模式,如果你不太了解,你可以先看看这篇文章。饥饿模式和懒惰模式
看完这篇文章,你会发现在单例模式下保证线程安全的最佳方案应该是DCL,在此,笔者复制其代码进行后续分析。
public class SingletonLazy {
private static SingletonLazy singleton;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
//再次判断当前singleton是否已经创建,如果创建,直接获取
if (singleton == null) {
synchronized (SingletonLazy.class) {
//先判断当前对象是否已经创建
if (singleton == null) {
singleton = new SingletonLazy();
}
}
}
return singleton;
}
}
public class SingletonTest {
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
SingletonLazy instance2 = SingletonLazy.getInstance();
System.out.println(instance==instance2);//true
}
}
在这里笔者想提一个问题,最上面的那个if语句能不能去掉?
我们知道加锁是一个非常耗资源耗性能的事情,如果说加锁需要100ns,那么if语句只需要1ns。如果有1W个线程同时进来,此时去掉了最上面的if语句的话,那么就需要对这个1W个线程进行校验也就是说需要对这1W个线程全部加锁,那么造成的性能损耗是非常严重的。而加了if语句后,可以方便的对这个1W个线程进行判断。
volatile关键字
Java并发编程有3大特性,即可见性,有序性,原子性。如果你看过《深入理解Java虚拟机》这本书你就应该知道在Java内存模型中始终存在乱序问题,因此可见性和原子性可以保证,但有序性却很难保证。
voltalie关键字大家应该比较熟悉,它有两方面的作用:保证可见性和禁止重排序。
那么它是如何做到的呢?其实归根到底就是Lock 指令。(这是汇编指令,这里不细讲,后续笔者会对缓存一致性协议进行相关文章编写的时候会讲到)
指令重排序
指令重排序(处理器的乱序执行)是指源码顺序和程序顺序不一样,指令重排序不是必然发生的,指令重排序会导致线程安全问题。(也就是说单线程结果的最终一致性没有问题,可以随便换)
在单线程环境下指令重排序没有影响,但在多线程环境下指令重排序很有可能会乱序执行。前面讲到的volatile关键字可以避免指令重排序。
最后
人生有三大境界:看山是山,看山不是山,看山还是山。笔者能力有限,只能写出这么多,如有不当之处还请多指教。