单例模式很简单,但是要写好不简单,一个主要的原因是要考虑并发场景下如何安全的创建单例
基本的思路是
- 隐藏单例类的构造函数,防止通过new的方式创建单例
- 通过暴露静态方法来提供实例
根据单例创建的时机分为两类:
- 如果是在类加载的时候就创建(Eager Initialization),称为饿汉式
- 或者是在类使用的时候再加载(Lazy Initialization),称为懒汉式
1 |
|
1 |
|
1 |
|
还有一种不常用的方式,仅仅作为学习参考
这种方式在java5以前比较流行,因为之前的synchronized方法性能较差,所以没有直接在方法上加synchronized关键字,而是使用双重检查(Double-checked Locking)的方式来进行懒加载.
1 |
|
关于双重检查写法中instance属性必须用volatile修饰的解释
前提
首先假设去掉第8行代码的volatile修饰
上述第13-18行是同步代码块,
第11,20两行代码没有同步措施。
根据happens-before规则,监视器解锁操作 happens-before 同监视器的加锁操作,如果一个操作 happens-before 于另一个操作,那么我们说第一个操作对于第二个操作是可见的,因此第13-18行代码内部是可以保证可见性的,但是第11,20两行是没有可见性保证的。
第一种情况 线程A执行完了13-18行的代码,并且释放了监视器锁, 此时线程B开始执行第11行代码,因为没有可见性保证,所以线程B可能看到instance等于null或者不等于null,存在这两种情况,执行到第20行的时候同样可能返回的是null或者不是null
第二种情况 线程A执行到了第16行代码,并且变量的初始化和赋值发生了指令重排序,造成instance已经被赋值,但是构造函数尚未执行完毕。 此时线程B开始执行第11行代码,同样因为没有可见性保证,线程B可能看到instance等于null或者不等于null, 不等于null的这种情况会更糟糕,因为此时构造函数还未执行完毕,第17行返回的将是一个部分创建的对象(partially constructed object)
参考:
《Java Concurrency In Practice》第16章
https://javadoop.com/post/java-memory-model
stackoverflow