对几种单例模式写法的理解

283 阅读5分钟

前言

单例模式是设计模式中非常基础的一种模式,有多种写法。本文主要分析常见的几种写法的优缺点进行简单的分析和说明。

单例模式,顾名思义就是这个类只有一个实例,并且只具有私有的构造器,外部无法通过类的构造器来创建实例。

常见的五种写法

1. 饿汉式

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

饿汉式的写法,getInstance() 方法不需要加同步关键字 synchronized,因为实例已经在类加载的时候已经创建好了,所有的线程调用 getInstance() 方法,都拿到的是同一个实例。

2. 懒汉式

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {
    }

    public synchronized static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

懒汉式的写法,getInstance 方法需要加同步关键字 synchronized,确保同一时刻只有一个线程进入 getInstance 方法代码块。

懒汉式的写法相当于延迟创建实例。有一个缺点,客户端每次调用 getInstance 方法时都做了同步,即便 INSTANCE 已经不为 null 了,导致会有性能损耗。

3. 双重检查加锁

public class Singleton {
    private volatile static Singleton INSTANCE; //volatile 关键字

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (INSTANCE == null) { //第一次检查
            synchronized (Singleton.class) {
                if (INSTANCE == null) { //第二次检查
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

双重检查加锁的版本,可以理解为懒汉式写法上的一种改进。解决了懒汉式每次调用getInstance 方法都需要同步的缺点,性能上会比懒汉式有更好的表现。

有3个地方需要注意:

  • 第一次检查,当实例被创建后,即 INSTANCE != null时,调用 getInstance 会直接返回单实例,没有做同步,这就是相比懒汉式写法优化的点。
  • 第二次检查是必须的,假如没有第二次检查,两个线程 A, B 同时通过第一次检查后,到达同步块外边,线程 A 拿到同步锁创建实例,释放锁;接着,线程 B 拿到同步锁,再次创建一个实例。就创建了不止一个实例,违背了单例的原则。
  • volatile 关键字修饰 INSTANCE 静态域。加了这个关键字后,能够保证调用 getInstance 方法时,各个线程里面 INSTANCE 的状态保持同步。例如线程 A 设置了 INSTANCE 的值,那线程 B 马上就会知道,避免了状态不同步导致创建多个实例的可能。

4. 静态内部类

public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

第一次调用getInstance方法时,Holder.INSTANCE第一次被读取,静态内部类Holder得到初始化,Holder下的静态域被初始化。只会在 JVM 装载Holder类的时候初始化一次,并由 JVM 来保证它的线程安全。

这里线程安全是通过 JVM 的类装载机制保证的,所以在 getInstance 方法定义时不需要添加synchronized 关键字。

5. 枚举

public enum  Singleton {
    INSTANCE;

    public void method() {}
}

枚举单例实现相比双重检查加锁和静态内部类的实现,要简洁很多。双重检查加锁需要保证线程安全,所以很多代码都是在处理同步问题。

静态内部类的实现,新增了一个静态内部类,利用 JVM 的类装载机制来保证线程安全,代码量上也会有所增加。

而枚举单例的线程安全不需要我们关心,根本上也是利用了 JVM 的类装载来保证线程安全。

枚举实现原理

通过 javac 编译 Singleton.java 文件,生成了一个 Singleton.class 文件,再通过 jad 反编译工具对 Singleton.class 文件进行反编译,反编译后的代码如下:

public final class Singleton extends Enum
{

    public static Singleton[] values()
    {
        return (Singleton[])$VALUES.clone();
    }

    public static Singleton valueOf(String s)
    {
        return (Singleton)Enum.valueOf(test/Singleton, s);
    }

    private Singleton(String s, int i)
    {
        super(s, i);
    }

    public void method()
    {
    }

    public static final Singleton INSTANCE;
    private static final Singleton $VALUES[];

    static 
    {
        INSTANCE = new Singleton("INSTANCE", 0);
        $VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

通过上面的反编译代码可知,枚举类型编译后,会生成一个继承自 Enum 的类,并且枚举的单实例被编译成了一个静态域,在这个类的 static 块里面初始化这个静态域。

static 块会在类第一次被加载的时候执行,而 JVM 装载类是线程安全的,所以枚举单例根本上还是利用了 JVM 的类装载机制来保证线程安全,跟静态内部类的机制有点相似。但枚举写法的代码量上要少很多。

总结

单例模式有多种写法,围绕的核心问题就是怎样解决线程安全和性能问题,保证单例的特性,并且在性能有良好的表现。

  • 饿汉式写法实例初始化太早,在不需要的时候也会进行实例化,可能导致资源消耗;

  • 懒汉式写法延迟加载了实例,但每次获取实例时,都要进行同步,性能上会有消耗;

  • 双重检查加锁写法是对懒汉式的改良,只有通过第一重检查后,才会进行同步。如果实例已经被创建,后面再调用获取实例方法,不会再进入同步块。代码量相对比较多。

  • 静态内部类写法是利用了 JVM 装载类机制的线程安全特性,所以我们不需要处理同步相关的逻辑,JVM 已经帮我们处理好了。代码量相对比较多。

  • 枚举单例底层也是利用了 JVM 装载类是线程安全特性,代码量非常少。另外,对于类实现 Serializable 情况,枚举的反序列化不是通过反射实现,所以也就不会发生由反序列化导致的单例破坏问题。

对于实现序列化和反射破坏单例的情况,本文不涉及,感兴趣的同学可以找找相关资料。