单例可以说是设计模式中最简单的一种模式。但任何一种设计模式都是普遍经验的总结,都有值得思考的地方。所以单例也并不简单,下面让我们慢慢了解它。
单例顾名思义这个类只有一个实例。要做到这点,需要做到以下几点:
(1)构造器私有化
(2)实例只在类内部创建,并且只会创建一次
(3)提供静态方法以供外部获取单例对象
单例有很多种实现方式:
先介绍两种最简单的:懒汉式和饿汉式
(一)懒汉式:顾名思义,这个单例很懒,懒者做事就慢。所以单例对象是等要用的时候再进行实例化的。下面是一个简单的例子
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (Objects.isNull(instance)) {
instance = new Singleton();
}
return instance;
}
}
(二)饿汉式:我们经常听到老一辈说别人吃饭很急,就会说"像个饿死鬼投胎一样",所以饿汉表现就比懒汉显得急迫,在类加载时就开始实例化。
public final class Singleton {
private static final Singleton SINGLETON = new Singleton();
private Singleton() {
}
public static Singleton getSingleton() {
return SINGLETON;
}
}
那么这两种有什么区别呢?
我们应该注意到,单例因为在程序中只有一个实例,所以是线程共享的。那么肯定是在多线程环境下。而懒汉模式是线程不安全的,想象一下多个线程同时在单例还没实例化时进入,那么Objects.isNull(instance)判断为true,从而都会创建实例然后返回,那么有可能返回的是不同的单例。所以懒汉模式几乎不会使用,以此就衍生了线程安全的懒汉模式(这个稍后再介绍)
而饿汉模式虽然因为类加载只会发生一次,而保证了线程安全。但可能会造成内存浪费。比如单例中可能定义了一个常量,而常量在业务场景中可能先于获取单例使用,那么在常量使用时就会进行单例实例化,如下面代码所示:
public final class Singleton {
private static final Singleton SINGLETON = new Singleton();
public static final String SINGLETON_CONTENT = "singleton content";
private Singleton() {
System.out.println("Singleton intance");
}
public static Singleton getSingleton() {
return SINGLETON;
}
public static void main(String[] args) {
System.out.println(Singleton.SINGLETON_CONTENT);
}
}
(三)线程安全的懒汉式
想要线程安全,我们首先想到的就是加synchronized锁,保证它的线程安全。于是就有了下面的实现
public final class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (Objects.isNull(instance)) {
instance = new Singleton();
}
return instance;
}
}
但这种方式虽然线程安全了,但它的锁是方法级的。意味着每次获取实例就会加锁,性能却成了问题。于是又再次进化,有了双重校验锁的方式,可以实现高性能且安全的单例模式。
(四)双重校验锁
上面已经说道双重校验锁是进化版的线程安全且高性能的懒汉模式。所以它也是需要用到的时候再实例化,但它是线程安全的。那么它是怎么做到的呢?我们直接上代码:
public final class Singleton {
private static volatile Singleton instance;
private Singleton() {
System.out.println("Singleton intance");
}
public static Singleton getInstance() {
// 第一次校验
if (instance == null) {
synchronized (Singleton.class) {
// 第二次校验
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如上代码所示,所谓双重检验锁,就是以两次校验+synchronized加锁方式实现的。第一次校验是基于性能考虑,避免每次都进行加锁,因为只有还没实例化时这个短暂且特殊的场景是需要加锁的,大部分时候都是直接返回实例就行。而第二次校验则是保证实例只会创建一次。
另外这里有个关键点,instance单例缓存是加了volatile关键字。这是用到了volatile中的其中一个特性--禁止指令重排,想了解更多关于volatile关键字,可以参看我的另一篇《浅谈volatile》
因为实例的创建实际会分为三个步骤
(1)分配内存
(2)初始化对象
(3)将对象指向刚分配的内存空间
如果实例是按这个顺序执行,那么不加volatile,范围的单例也是完成初始化的实力,并无影响。但因为编译器优化,可能会把因为初始化放到最后,提前先将对象指向刚分配的内存空间。那么在单例还没初始化完成时,其他线程进入获取单例时,引用对象就不是空,而提前获取到还没初始化完成的单例引用,就可能会导致后面的业务逻辑出错。
(五)静态内部类
public final class Singleton {
private Singleton() {
System.out.println("Singleton intance");
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
public static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
静态内部类也是懒加载的模式,但它的实现更优雅,是我比较推荐的方式。它利用静态内部类可以访问外部类私有构造器的特性,将单例的实例化延迟到静态内部类类加载时,保证了单例的线程安全。而这个静态内部类只会在获取单例的时候加载,所以也是懒加载。
(六)枚举方式
public enum Singleton {
INSTANCE;
Singleton() {
// 初始化
}
}
枚举方式实现也很简单,但却最安全,因为它是反序列化。这个是前面的方式做到的。
上面介绍了六种单例实现方式。除了第一和第三种的懒汉式不推荐使用外,其他可以根据实际需要选择。比如通常单例比较简单,那么通常饿汉模式就可以满足。需要延迟加载,那么双重校验锁、静态内部类、枚举类都可以。如果涉及到序列化,那么枚举就是唯一选择了。