一、概述
单例模式(Singleton Pattern)的定义:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式是一种对象创建型模式。
它的使用场景如下所示:
- 整个项目需要一个共享访问点或共享数据。
- 创建一个对象需要消耗的资源过多,比如访问I/O或者数据库等资源。
- 工具类对象。
在本篇笔记中,将会介绍以下6种单例模式的实现方式:
- 懒汉模式(线程不安全)
- 懒汉模式(线程安全)
- 饿汉模式
- 双重检查模式(DCL)
- 静态内部类单例模式
- 枚举单例
二、实现方式
首先看到最简单,但是也是唯一一个线程不安全的实现——懒汉模式(线程不安全)。
1. 懒汉模式(线程不安全)
这是一种最基本的实现方式,但它是线程不安全的,也就是说只能在单线程的情况下提供正确的单例输出。由于没有加 synchronized
同步锁,严格意义上讲它并不属于单例模式。
之所以叫做懒汉模式(lazying loading)是因为它是在用户第一次调用的时候才会进行初始化,这样子做可以节约部分内存。
它的实现如下:
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
2. 懒汉模式(线程安全)
上面提到的懒汉模式的实现方式是线程不安全的,而要实现线程安全其实非常容易,只需要在 getInstance
方法上加 synchronized
关键字即可,只要锁住了该对象,那么在多线程的情况下它也是安全的,具体实现如下:
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
虽然这种方式能够在多线程的模式下很好地工作,但是在每次调用 getInstance
方法的时候都需要进行同步,但在大多数情况下我们是用不到同步的,所以这会造成一些不必要的同步开销,一般不推荐采用此方法。
3. 饿汉模式
懒汉模式是在用户第一次调用时才进行实例化,那么饿汉模式就是与之完全相反的——在类加载的时候就已经完成了初始化。它的优点是获取对象的速度快,并且基于类加载机制的模式使其避免了多线程的同步问题,所以它是线程安全的。但它的缺点也显而易见,无论有没有使用该单例实例,在类加载的时候都完成了实例化,如果从始至终都没有使用该实例,那么就会造成内存浪费。
它的实现如下:
public class Singleton {
// 类加载的时候就完成了初始化
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
4. 双重检查模式(DCL)
双重检查模式(Double-Check Loading)也是懒汉模式的一种,它会对 Singleton
进行两次判空。需要注意的是使用这种模式实现单例的时候需要在静态成员变量 instance
之前增加修饰符 volatile
,以防止在极端情况下单例失效。它的实现如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
第一次判空是为了避免不必要的同步,因为在单线程的时候我们是不需要进行同步保证的。而第二次判空在同步块内,当 instance
依然为空时,才会去调用构造方法创建对象,这样就能保证线程安全了。
至于为何对 instance
进行 volatile
关键字的修饰,主要原因是防止一种极端情况的发生。我们都知道为了提供程序的性能,JVM 会为我们的代码做一些优化,即指令重排序。我们分析一下第 11 行的代码:instance = new Singleton();
,虽然只有一行,但是被编译后它会变成以下 3 条指令:
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚才分配的内存地址
正常情况下,如果这 3 条指令按如上 1、2、3 的顺序进行执行,那么 DCL 就不会出现问题。但 CPU 内部为了提高执行效率,在不影响最终结果的前提下,可能会对上述指令进行重新排序:
memory = allocate(); // 1.分配对象的内存空间
instance = memory; // 3.设置instance指向刚才分配的内存地址
ctorInstance(memory); // 2.初始化对象
这里要提一点:
不影响结果的意思是不影响单线程的最终结果,以上述例子为例,在单线程的情况下,2 和 3 之间不存在先行发生关系(即它们之间是互不依赖的)。也就是说在单线程下,先初始化对象再将instance指向分配的内存地址或先将instance指向分配的内存地址在初始化对象最终得到的结果都是一样的。
但是在多线程中,考虑这样的一种情况,在线程 A 调用 getInstance
方法获取实例的时候,假设这时候进行了指令重排序,这是 A 执行了1和3,还未执行2,所以 instance
还未初始化。而此时另一个线程 B 抢占了 CPU 资源,执行到8行,此时判断为 false
,那么它就会直接跳到第15行返回 一个还未初始完的 instance
实例,那么此时就会出现问题。
而 volatile
则可以禁止指令的重排序,从而避免了这种极端情况的发生,虽然损失了一些性能(重排序可以优化性能),但是考虑到程序的正确性这点损失还是值得的。
5. 静态内部类单例模式
我们知道,饿汉模式不能实现延迟加载,无论用不用都会占据内存,而懒汉模式为了线程安全的考虑则添加了同步锁 synchronized
,影响了性能。而静态内部类单例模式则是克服了以上两种实现方式的缺点,采用了静态内部类的形式提供单例:
public class Singleton {
private Singleton(){
}
private static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder{
private static final Singleton sInstance = new Singleton();
}
}
由于 sInstance
不是 Singleton
的成员变量,所以在类加载的时候它并不会被初始化,直到调用 getInstance
方法的时候,虚拟机加载 SingletonHolder
并初始化 sInstance
。它的线程安全通过虚拟机的类加载机制进行保证,并且延迟加载的机制也解决了饿汉模式下的问题,所以这是一种比较推荐的单例模式实现方式。
6. 枚举单例
枚举单例的实现如下所示:
public enum Singleton {
INSTANCE;
public void doSomething(){
}
}
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,它更加简洁,自动支持序列化机制,防止了反序列化机制,防止多次实例化。它是最佳的单例实现模式,但是在实际中,这种单例的实现方式很少被使用到。
对于单例模式的总结就到此结束了,希望这篇博客能够对您有所帮助~
有问题的话可以在评论区下方给我留言。
参考
本篇博客参考自以下书籍和博客:
《Android 进阶之光》第6章 设计模式