前言
使用单例模式时,一般有两种选择,一个是懒汉式,一个是饿汉式。但是这两种都是有各自的缺点,无法满足我们的需求,所以DCL(Double Check Lock双端检锁机制)出现了,一种既支持延迟加载、又支持高并发的单例实现方式
什么是线程安全?
当多个线程同时访问一段代码的时候,代码的执行结果不会因为线程的访问顺序不同从而产生不正确的结果,这段代码就是线程安全的;要保证线程安全需要保证原子性、可见性、有序性;
双重检锁下面的单例模式 - 为什么使用双重检锁?
使用双重锁的目的就是为了创建出来的单例是唯一的(唯一性)
,并且是线程安全的(满足原子性、可见性、有序性)
;
public class Singleton {
// 为什么使用 volatile?
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
// 为什么在外面进行一次 if 判断
if (singleton != null) {
return singleton;
}
synchronized (Singleton.class) {
// 为什么在此处再次进行非判断?
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
}
解决问题一
在 JVM 中,执行 singleton = new Singleton();
是存在可能发生指令重排的;执行这一句代码在字节码指令中分为三个步骤:
1、开辟一个内存空间,用来存放将来的初始化出来的对象
2、对象的初始化
3、栈的指针指向堆内存中创建的对象
步骤 2 3 在多线程访问的时候,由于发生了指令重排,可能导致一个线程Thread - 0 创建出来了的执行顺序是:
1、开辟一个内存空间,用来存放将来的初始化出来的对象
2、栈的指针指向堆内存中创建的对象
3、对象的初始化
此时 Thread - 0 到第二步骤的时候,时间片用完了,第三步没有来的及执行(也就是对象没有被初始化,对象没有被创建出来,只是有了给对象分配的内存空间,内存空间中是空的)
;此时 Thread - 1 来了;
Thread - 1线程访问的时候,发现栈的指针已经指向了一块内存空间,所以停止了创建,但是此时的对象并没有初始化,会发生空指针异常的情况;
所以使用了 volatile 预防了指令重排序的情况
解决问题二
为什么在 synchornized 的外面进行一次 if ?
因为这样代码块放在了外面,减少了代码同步的范围,使得性能得到了一定的增加;
只是第一第二个线程访问的时候,需要使用同步代码块,后面的线程访问到的时候,都是不需要进行访问同步代码块的,因为在第一第二次已经创建好对象了;不需要进入同步块就可以知道单例对象有没有创建;相反如果每次都进去同步块确认一次,系统的开销是比较大的;
解决问题三
在 synchornized 里面为什么还是需要一次 if 判断是因为:防止多次的创建对象,多次创建就不符合单例模式了;
假设 Thread - 0;进入了 synchornized 代码块中,此时还没有创建对象;
此时 Thread - 1;也来了也在 synchornized 代码块外面等着(它来的时候也没有对象第一次的 if 已经判断过了)
,此时如果不在 synchornized 继续加一个 if 判断的话,Thread - 1 会继续创建出来一个对象;