懒汉式:跟饿汉式在类加载时创建不一样,懒汉式是在我们第一次使用时才创建
线程1
和线程2
同时调用了getInstance
方法,他们都走到了②,由于INSTANCE
并没有被创建,所以②处的if判断,线程1
和线程2
都为true,他们都可以走到③线程1
和线程2
同时走到了③,此时就会发生锁的竞争,这里我们假设是线程1
抢到了锁,能接着往下执行,线程2
则在③等待,等待线程1
释放了锁后再执行后续代码线程1
走到④的时候,想必大家都有疑问,咋又来判断if(INSTANCE == null)
呢?后面再给大家解答,我们先接着往下走线程1
在④进行if判断的结果肯定为true,所以线程1
能走到⑤,这里敲重点,让我们的视线看向①,我们在INSTANCE
前加了volatile
关键字,这个关键字的作用我先不讲,我们先把流程梳理完后再来讲线程1
走完⑤后,INSTANCE
就会被创建出来了,这时候线程1
就可以释放锁,拿着创建好的INSTANCE
退出getInstance
方法去执行自己的业务逻辑线程1
从执行②到执行完⑤的这个期间内
,可能会有其他的线程例如线程3、线程4、线程5
也调用了getInstance
方法,这三个新的线程肯定都会和线程2
一样卡在③等待线程1
释放锁线程1
释放了锁,也就是创建好INSTANCE
后,线程2、线程3、线程4、线程5
就会抢锁,无论它们之间谁抢到了锁,都会来到④,这个时候,INSTANCE
已经不为空了,他们都会拿着线程1
实例化好的INSTANCE
直接返回了,就不会再重新new Singleton()
了,这也就解答了step 3
提出的问题,即为什么还需要一个if判断。②③④就构成了双重检查锁线程1
执行完⑤后,如果有新的线程例如线程6
调用了getInstance
方法,那么线程6
走到②就为false,直接拿着线程1
创建好的INSTANCE
返回,即后续所有新的线程调用getInstance
就都不会走到③了,也就不用加锁和释放锁了,也就解决了上一章在方法上加synchronized
导致频繁加锁和释放锁以及锁的粒度大的问题INSTANCE
前加volatile
关键字,大家看向⑤,其实new一个对象的过程总共涉及三步,并不是一蹴而就的
INSTANCE
Singleton
类中有int a; int b; double c;
这三个成员变量,然后再往下看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
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
}
}
上一章和本篇我们讲了单例模式-懒汉式的两种实现方式,相信大家对懒汉式都有了一定的理解
好了,Java设计模式-创建型模式-单例模式-懒汉式就讲到这里了~
大家有什么问题或者建议,欢迎留言和讨论,虽做不到秒回(哈哈,没办法,要先工作 o(^ヘ^o),但会当天回复~