文章目录

  • 单例模式
  • 饿汉式
  • 懒汉式
  • 双重检查锁
  • 静态内部类
  • 枚举


单例模式

按照惯有的分类方式,设计模式总共分为3大类:1、创建型 ,2、结构型, 3、行为型。

单例模式便是创建型设计模式的一种,它确保某一个类在系统中只有一个实例,并自行实例化,同时向外部提供获取这个唯一实例的接口。从这段描述中,我们不难可以得到单例模式的三大特性:

  • 单例类只有一个实例。
  • 单例类必须自己实例化自己。
  • 单例类需要向外提供实例。

虽然单例设计模式算是“入门级“的设计模式,但依然需要我们仔细去理解它的特性是如何通过代码实现,这些代码背后的原理又是什么。

下面,本文通过分析5中经典的单例模式写法,逐步分析写法的成因与背后原理。

饿汉式

public class EagerSingleton {
        // 静态变量,类在创建之初就会执行实例化动作。
        private static EagerSingleton instance = new EagerSingleton();
        // 私有化构造函数,使外界无法创建实例
        private EagerSingleton(){}
        // 为外界提供获取实例接口
        public static EagerSingleton getInstance(){
            return instance;
        }
    }

上面是饿汉式单例模式的标准代码,所谓的“饿汉式”只是形象的比喻:EagerSingleton类的实例因为变量instance申明为static的关系,在类加载过程中便会执行。由此带来的好处是Java的类加载机制本身为我们保证了实例化过程的线程安全性,缺点是这种空间换时间的方式,即使类实例本身还未用到,实例也会被创建。

饿汉式的缺点有2:

  • 空间使用率不高
  • 类加载时实例化,意味着该类无法在程序运行过程中通过运行参数实例化,代码失去灵活性。

饿汉式在当前硬件设备条件下,缺点其实关系不大,对于空间不是特别严苛的应用来说,且用不到初始化参数的类型来说,我非常建议使用这种方式。

懒汉式

“懒汉式”是针对饿汉式单例模式缺点而生的懒加载模式,所谓懒加载的意思是,只有当需要使用实例的时候才去实例化。来看看示例代码:

public class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton(){}
    // 为外界提供获取实例接口
    public static LazySingleton getInstance(){
        if(instance == null){
            instance = new LazySingleton(); // 懒加载
        }
        return instance;
    }
}

饿汉式和懒汉式的区别在于,饿汉式在类加载时便被实例化,而懒汉式是在getInstance()函数调用时,相信你也能看出来,当instance == null 时,去实例化,否则直接返回实例。

但这里有个问题,单例模式的核心是系统中只存在一个单例类的实例,这其实隐含了实例只创建一次的意思。但上述LazySingleton类只能保证在单线程中只创建一次,在多线程中却不能保证。

如果有两个线程,Thread1Thread2,两个线程先后调用getInstance()函数。如果Thread1的调用,执行到if(instance == null)的语句块中被中断,此时instance的值还未改变,Thread2也执行到了这里,可以预见,两个线程都将分别创建一个LazySingleton实例,最终instance的值是那个线程创建的实例,将是不确定的。

这个缺点的原因,涉及到并发编程的原子性。实例中,创建实例的代码逻辑失去了原子性从而导致可能存在多个实例创建的情况。

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

既然这样,我们给实例代码加上原子性就好了。

public class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton(){}
    // 为外界提供获取实例接口
    public static synchronized LazySingleton getInstance(){
        if(instance == null){
            instance = new LazySingleton(); // 懒加载
        }
        return instance;
    }
}

synchronized是Java中实现代码块原子性的关键字之一,getInstance()函数加上了原子性后,确实解决了问题。但这又引入了新的问题。

getInstance()函数申明上加synchronized,意味着每次函数调用都会进行同步检查,这是低效的。实际上,我们只需要保证创建实例代码的原子性即可。即:

if(instance == null){
    instance = new LazySingleton(); // 懒加载
}

也就是说,这种实现方案的同步范围扩大了,这个问题由双重检查锁来解决。

双重检查锁

在前面,我们在getInstance()加了synchronized,扩大了同步范围,现在我们来减小一下同步范围:

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        //先检查实例是否存在,如果不存在才进入下面的同步块
        if(instance == null){
            //同步块,线程安全的创建实例
            synchronized (Singleton.class) {
                //再次检查实例是否存在,如果不存在才真正的创建实例
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

所谓的双重检查,是在同步前后的两次if(instance == null)判断,是否已经存在实例,自然指的就是synchronized关键字。

对着代码,我们再来考虑两个线程同时通过了第一道if(instance == null)检查,但因为同步锁是互斥的,只能第一个线程释放后,第二个线程才能持有。这保证了同步代码块的原子性,在同步代码块中的如果instance还为创建,此时才会创建。

此外,还需要注意的是volatile关键字的使用。

instance = new Singleton();这行代码执行时,虚拟机大概可以分为三个指令步骤:

  1. 在内存中给Singleton实例分配空间
  2. 调用Singleton构造函数,初始化成员
  3. 为Singleton实例指向第一步分配的内存空间(此时instance不为空)

代码在编译时,存在指令优化的现象。指令优化只保证单线程条件下执行结果一致,而不保证执行的顺序。所以前面三个指令的执行顺序是不确定的,可能是1-2-3,也可以是1-3-2。如果顺序是1-3-2,当第三步执行完后,instance已经不为空了,但成员并未初始化,第二个线程使用该instance自然会报错。怎么解决呢?

volatile可以解决这个问题,该关键字可以确保相关变量涉及的代码指令不被优化顺序。

来看看双重检查锁的代码,即实现了线程安全,也实现了懒加载。已经很完美了,唯一的缺点是,有点儿太复杂。

静态内部类

看到这里,应该也明白了,最好的单例现实,需要满足两个条件:1. 线程安全。2. 懒加载。在前面,这两点都被我们手动一一实现。可不可以不用自己手动实现呢?当然,来看下面的代码。

public class Singleton {
    private Singleton(){}
	// 只有当类被调用时,才会加载
    private static class SingletonHolder{
        // 静态初始化器,由JVM来保证线程安全
        private static Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
  • 线程安全:由静态内部类中的静态成员初始化时创建实例,通过JVM类加载机制来保证线程的安全性。
  • 懒加载:使用静态内部类的方式,让类SingletonHolder只有在使用的时候才会被加载,实例才会创建,借机实现了懒加载。

那么还有没有更简单的呢?

枚举

public enum Singleton {
    uniqueInstance;
    public void singletonOperation(){
        // 单例类的其它操作
    }
}

虽然《高效Java 第二版》中说,单元素的枚举类型是实现单例的最佳方法。

虽然说使用枚举的方式确实简洁方便,又不怕出错,但我觉得还是不能满足这一点:无法在程序运行过程中通过运行参数实例化,代码失去灵活性。