众所周知,单例模式是所有设计模式中最常运用且简单的设计模式,单例模式有多种写法(变种写法),自己归纳了六种写法:饿汉式、非线程安全懒汉式、线程安全懒汉式、DCL双重校验锁懒汉式、静态内部类懒汉式、枚举类式。
下面,我们会使用Java和Kotlin两个开发环境下实现每一种单例模式。 单例模式有以下几个特点:
- 单例只能有一个实例
- 单例必须自己创造自己的唯一实例
- 单例必须提供全局的唯一实例
在单线程中的案例介绍前,要着重解释下立即加载 和延时加载 的概念。
- 立即加载:在类加载初始化的时候就主动创建实例。
- 延时加载: 等待使用时就去创建实例,不用则不创建 。
在单线程环境下,单例模式根据实例化对象时机的不同,有两种经典的实现:一种是 饿汉式单例(立即加载) ,一种是 懒汉式单例(延迟加载)。 饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;而懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。 下面我们深入浅出的谈谈关于单例模式的几种写法,针对每种写法在日常开发中会对全局产生哪些深远的影响呢。
一、饿汉式
1、Kotlin环境下的饿汉式:
1 |
|
2、Java环境下的饿汉式:
1 |
|
我们知道类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
在多线程并发的条件下,有以下代码:
1 |
|
由上面代码可知,饿汉式单例是一个线程安全的单例模式,为什么这么说呢?因为饿汉式在初始化实例时,预先就由类加载器将类字节码文件传给JVM触发初始化操作,就会实例化一个对象并交给自己的引用,供系统使用。换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例是线程安全的。
二、传统懒汉式
1、kotlin环境下的传统懒汉式:
1 |
|
2、Java环境下的传统懒汉式:
1 |
|
我们从传统懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。 在多线程并发作用下,使用传统懒汉式有以下代码:
1 |
|
由上面代码可知,在多线程并发操作下,无法保证线程的单一性,究其原因是:存在多个线程同时进入if (field == null) {...}(Kotlin环境下)或 if (singleton == null) {...}(Java环境下) 语句块中,在同一时间内多个线程可能无法对field == null判断,当某个时间点满足了field == null自然就不会重新创建实例。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的,若想要使得传统的懒汉式单例变成线程安全,就需要对它进一步的改良和优化。
三、线程安全懒汉式
通过上面的非线程安全懒汉式,我们对其进一步的改良,通过在向外提供的静态方法块中添加一个同步锁Synchronized,以达到线程安全的目的。
1、kotlin环境下的线程安全懒汉式:
1 |
|
2、Java环境下的线程安全懒汉式:
1 |
|
那么它在多线程并发的时候代码如下:
1 |
|
由上面代码可知,通过在向外提供的get() or getInstance()方法块中添加Synchronized确实实现了线程安全,但这中方法有一个缺点,那就是当这个实例在多线程中实例化时,每次都要同步这个方法块中的代码,虽然保证了线程安全,但是去了效率,所以我们可以对其进一步的优化。
四、DCL双重校验锁懒汉式
通过上一个例子我们已经知道,在方法块中添加同步锁Synchronized是可以保证线程安全的,但效率低,这次我们针对上一个案例做一个改良。
1、Kotlin环境下的DCL双重校验锁懒汉式:
1 |
|
2、Java环境下的DCL双重校验锁懒汉式:
1 |
|
通过上面代码可知,这次的改良是通过双锁实现线程安全,同时也保证了获取实例的高效性,为什么这么说呢?因为当这个实例在多线程并发下,首先走了 第一个锁if (instance == null) {…}代码块,当instance为null时直接进入第二个锁,第二个锁是初始化获取这个实例的代码块,于此同时使用一个同步锁,保证线程安全,当第二次在获取实例时就不走 if (instance == null) {…}代码块代码了,直接拿到之前初始化得到的实例,就不用每次进入同步代码块中获取实例,既保证了同步的同时又保证了获取实例的高效性。
这里需要另外提一句,在上面代码中,你会留意到静态变量加了一个volatile关键字,这个关键字是DCL双重校验锁的最终的改良点,它的作用是防止指令重排,避免在多线程并发执行时,无法完整的去获取实例而导致空指针的情况。
在 if (instance == null) {instance = new Singleton();}中的new Singleton()是非原子性操作,可能会创建一个不完整的实例,它会使得指令重排,一般的我们初始化对象的执行操作会分以下三步:
- a.分配对象内存空间。
- b.初始化对象。
- c.使得我们所初始化的对象(这里是Singleton对象)指向刚分配的内存地址。
如果不使用volatile去修饰静态变量instance,这就会导致在多线程中执行时,可能是直接按照上面的指令顺序(a->b->c)执行的,这种情况下就可以完整的获取到实例;也有可能会将指令重排,变成a->c->b的顺序,这种情况下在DCL单例中,误以为是有一个实例指向了内存地址,所以就不会走第一个锁if (instance == null){...},直接走return instance代码,而实际这个instance是空的,会报空指针异常。所以DCL双重校验锁中必须要使用volatile修饰。
五、静态内部类懒汉式单例
上面几个是针对传统懒汉式进行很明显的改良与优化,那么这个静态内部类则是一个优秀的传统懒汉式的变种模式(我是这么认为的)。
1、Kotlin环境下的静态内部类懒汉式单例:
1 |
|
2、Java环境下的静态内部类懒汉式单例:
1 |
|
为什么他是一个优秀的变种呢,首先,它是一个使用时就加载(懒加载),同时,私有内部中的实例对象按需加载,这样既保证了线程安全,更保证了获取实例的高效性,个人是很推荐这种方式的。
六、枚举类式单例
1、Kotlin环境下枚举类式单例:
1 |
|
2、Java环境下枚举类式单例:
1 |
|
枚举与其他几个线程安全的懒汉式单例一样也实现了线程安全,另外,前面讲的几种单例都可以被反射破坏,但枚举类式的单例除外,具体怎么破坏以及解决方案具体如下:
-
反射破坏单例(这里拿线程安全单例做例子):
archives.kotlinarchives.kotlin 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46fun main(){ //1.通过线程安全懒汉式初始化获取单例 val single1 = Singleton.getInstance() //2.通过反射获取的对象实例 try { val constructor =SingletonFourth::class.java.getDeclaredConstructor() constructor.isAccessible = true //通过反射创建SingletonFourth对象 val single2 = constructor.newInstance() //通过判断发现直接创建的对象和反射获取的对象不相等(违反了单例原则) Log.e("SingletonFragment", "single2 ${if (single2 == single1) "=" else "!="} single1") /** * * 得到的结果是:single2 != single1 * **/ } catch (e: Exception) { Log.e("SingletonFragment", e.message.toString()) } } *******************************我是分割线******************************* class Singleton private constructor() { companion object { private var singleton: Singleton? = null get() { if (field == null) { field = Singleton() } return field } //静态方法块中添加synchronized以保证线程安全 @Synchronized fun get(): Singleton { return singleton!! } }
-
解决反射破坏单例的方法:
archives.kotlinarchives.kotlin 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Singleton private constructor() { companion object { private var singleton: Singleton? = null get() { if (field == null) { field = Singleton() } return field } //静态方法块中添加synchronized以保证线程安全 @Synchronized fun get(): Singleton { //加上判断instance是否为null, if(instance!=null){ throw RuntimeException("实例已存在,请通过get()获取实例!") } return singleton!! } }
上面是kotlin的解决方法,Java的解决方法是在私有构造方法里面加上if(instance!=null{要抛出的异常}这句代码。
八、小结
本文首先介绍了单例模式的定义和结构,并给出了其在单线程和多线程环境下的几种经典实现。特别地,我们知道,传统的饿汉式单例无论在单线程还是多线程环境下都是线程安全的,但是传统的懒汉式单例在多线程环境下是非线程安全的。当然,实现懒汉式单例还有其他方式。但是,这五种是比较经典的实现,也是我们应该掌握的几种实现方式。从这五种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:
- 尽量减少同步块的作用域;
- 尽量使用细粒度的锁。