1.懒汉式
懒汉式:跟饿汉式在类加载时创建不一样,懒汉式是在我们第一次使用时才创建
2.懒汉式-"双重检查锁+volatile"实现
- 懒汉式用"双重检查锁+volatile"的实现方式如上图,重点代码我框了起来,并进行了编号
- 下面的步骤有点多,没办法,双重检查锁+volatile确实不好讲,细节也多,建议大家多看几遍,慢慢梳理流程
- 按②③④⑤的顺序梳理一下整体逻辑
- step 1:假设现在有
线程1
和线程2
同时调用了getInstance
方法,他们都走到了②,由于INSTANCE
并没有被创建,所以②处的if判断,线程1
和线程2
都为true,他们都可以走到③ - step 2:假设
线程1
和线程2
同时走到了③,此时就会发生锁的竞争,这里我们假设是线程1
抢到了锁,能接着往下执行,线程2
则在③等待,等待线程1
释放了锁后再执行后续代码 - step 3:
线程1
走到④的时候,想必大家都有疑问,咋又来判断if(INSTANCE == null)
呢?后面再给大家解答,我们先接着往下走 - step 4:
线程1
在④进行if判断的结果肯定为true,所以线程1
能走到⑤,这里敲重点,让我们的视线看向①,我们在INSTANCE
前加了volatile
关键字,这个关键字的作用我先不讲,我们先把流程梳理完后再来讲 - step 5:
线程1
走完⑤后,INSTANCE
就会被创建出来了,这时候线程1
就可以释放锁,拿着创建好的INSTANCE
退出getInstance
方法去执行自己的业务逻辑 - step 6:在
线程1
从执行②到执行完⑤的这个期间内
,可能会有其他的线程例如线程3、线程4、线程5
也调用了getInstance
方法,这三个新的线程肯定都会和线程2
一样卡在③等待线程1
释放锁 - step 7: 当
线程1
释放了锁,也就是创建好INSTANCE
后,线程2、线程3、线程4、线程5
就会抢锁,无论它们之间谁抢到了锁,都会来到④,这个时候,INSTANCE
已经不为空了,他们都会拿着线程1
实例化好的INSTANCE
直接返回了,就不会再重新new Singleton()
了,这也就解答了step 3
提出的问题,即为什么还需要一个if判断。②③④就构成了双重检查锁 - step 8:在
线程1
执行完⑤后,如果有新的线程例如线程6
调用了getInstance
方法,那么线程6
走到②就为false,直接拿着线程1
创建好的INSTANCE
返回,即后续所有新的线程调用getInstance
就都不会走到③了,也就不用加锁和释放锁了,也就解决了上一章在方法上加synchronized
导致频繁加锁和释放锁以及锁的粒度大的问题 - step 9:最后我们来看为啥要在
INSTANCE
前加volatile
关键字,大家看向⑤,其实new一个对象的过程总共涉及三步,并不是一蹴而就的- 1:分配内存空间,实例化对象(这个时候对象已经有了,但是对象里面的成员变量存的都是默认值)
- 2:初始化对象,即调用构造方法为成员变量赋实际值
- 3:将内存空间地址赋值给
INSTANCE
- 我们先假设
Singleton
类中有int a; int b; double c;
这三个成员变量,然后再往下看 - step 10:正常创建一个对象的步骤是1-2-3,如果不加
volatile
,可能会出现指令重排(指令重排是操作系统为了提高执行效率和资源的利用效率),指令重排可能会导致步骤变为1-3-2。在单线程的情况下,1-3-2没啥问题。如果是多线程情况下,就可能出现问题,假设没加volatile
,线程1
执行到⑤创建对象的时候发生了指令重排,即步骤变为了1-3-2,这个时候我们再假设线程1
只执行完了1-3,2还没来得及执行,即已经将内存空间的地址赋值给了INSTANCE
,但是INSTANCE
里面的成员变量没有赋实际值,都还只是默认值,重点
,重点
,重点
,这个时候突然来了一个线程10
调用了getInstance
方法,线程10
走到②的时候,if(INSTANCE== null)
为false,线程10
就拿到了这个没赋实际值的INSTANCE
,若是去访问里面的成员变量,那么就会出现问题。所以加volatile
是为了防止在多线程情况下创建INSTANCE
的时候发生指令重排,让创建INSTANCE
的步骤锁死为1-2-3,步骤锁死为1-2-3的话,只有在给成员变量赋好实际值之后,才会将内存空间地址赋值给INSTANCE
- step 1:假设现在有
3.代码测试
public class TestDemo {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("s1 == s2: " + (s1 == s2)); // s1 == s2: true
}
}
4.总结
- 优点
- 线程安全
- 支持延迟加载
- 支持高并发
- 缺点
- 实现复杂
5.饿汉式和懒汉式总结
- 饿汉式:如果系统中的单例大多采用饿汉式的话,那么这个系统启动的比较慢,因为要事先创建好所有的单例对象,资源的占用也比较多。但是这个系统的运行速度就要快些,因为需要的单例对象都已经创建好了,不需要在运行的时候创建
- 懒汉式:如果系统中的单例大多采用懒汉式的话,那么这个系统启动的比较快,因为不需要的单例对象不必事先创建,资源的占用就比较少。但是这个系统的运行速度可能一开始就慢一些,因为单例对象要在运行途中创建
上一章和本篇我们讲了单例模式-懒汉式的两种实现方式,相信大家对懒汉式都有了一定的理解
好了,Java设计模式-创建型模式-单例模式-懒汉式就讲到这里了~