单例模式

设计模式分为创建型模式、结构型模式和行为型模式。本文讲解单例模式,为创建型模式。


特点


单例模式有以下几个特点:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

由以上特点可以知道

单例类中的创建类的语句必须对外界屏蔽。体现出来的特点的就是:构造函数是私有的

创捷单例类的时候,要保证该类仅有一个实例,并提供一个访问它的全局访问点

下面我们来看看如何用不同的方式实现以上特点。


实现


首先要强调一点是:按单例类的实例化时机来划分的话,其实实现单例模式就只有两种模式——懒汉模式与饿汉模式。但由于普通的懒汉模式与饿汉模式都有或多或少一些的缺点。所以衍生出了很多种子模式。接下来我们来了解一下。


一、懒汉模式


懒汉模式是区别于饿汉模式的。不提前做好准备,不到用到的时候不实例化,给人很懒的感觉。所以称之为懒汉模式。而后面要提到的饿汉,没用到就已经实例化了,非常饥饿。

public class LazySingleton {

    private volatile static LazySingleton INSTANCE; // volatile禁止指令重排

    private LazySingleton(){}

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

上面的懒汉模式只能在单线程中使用,也就是说是非线程安全的。从上面普通懒汉模式的代码可以看出来,在多线程的情况下,实例创建语句可能会被执行多次。所以要实现线程安全的懒汉模式,最简单的方法就是的加synchronized关键字修饰。如下:

可以参考这篇文章,温习一下synchronized相关知识

public class LazySingleton {

    private volatile static LazySingleton INSTANCE; // volatile禁止指令重排

    private LazySingleton(){}

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

以上通过最简单粗暴的方式,的确可以实现线程安全 的0的,但熟悉的synchronize的同学相信知道该方式性能比较差。后面会介绍的一种效率比较高的实现方式——双重校验锁模式


二、饿汉模式


相比于懒汉模式的“延迟加载”,饿汉模式就是直接在类加载的时候就已经生成唯一的实例了。如以下代代码实现:

public class HungrySingleton {

    private static HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getINSTANCE() {
        return INSTANCE;
    }
}

三、懒汉模式与饿汉模式的区别


仔细品味懒汉模式与饿汉模式的实现代码,我们来的看以下两者的区别:

  • 1、懒汉模式,等到要用的时候再创建实例,比较耗时间,但节省空间,典型的时间换空间。

  • 2、饿汉模式,不管你用不用得到的,该类的唯一实例都已经在jvm加载该类的时候进行实例化了。

    实际上就是在类的连接过程进行内存分配且触发类的实例化,关于类的生命周期介绍,可以参考另外一篇文章Java类的生命周期浅析

  • 3、懒汉模式需要解决线程安全问题。而饿汉模式不需要。因为jvm在加载类的时候是单线程的。可以保证存在单一实例。


四、双重校验锁


双重校验锁是懒汉模式的一种延伸,也即是说对象的实例化是延迟执行的,它是为了解决上面所提到的普通的线程安全懒汉模式效率低下的问题。

该方式,其实同样为synchronized关键字加锁。但加了两层校验,故命名为双重校验锁。

来看下具体的代码实现

public class DoubleCheckSingleton {

    private static volatile DoubleCheckSingleton INSTANCE;

    private DoubleCheckSingleton(){}

    public static DoubleCheckSingleton getInstance(){
        if (INSTANCE == null){
            synchronized (DoubleCheckSingleton.class){
                if (INSTANCE == null){
                    INSTANCE = new DoubleCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

接下来我们来讨论一下该方式相对于synchronized对方法加锁实现的线程安全的区别在哪。

  • 首先我们要知道,我们对实例化过程加锁的目的,是在于当单例还没生成时,防止多线程生成多个实例。也就是我们想锁住的时机,其实是未生成实例的时候。但如果对整个方法进行加锁,则会导致但你生成实例,以后要每次要获取实例时,都会受到锁的影响。这并不是我们想要的。于是乎,我们有必要在加锁前,先加一层校验,来防止这个现象发生。
  • 至于有人会问,说为何里面还需要多一层检验的问题,其实通过的分析可以很容易得出:不加里层校验,并不能保证单例的结论。因为会存在多个线程都通过第一层校验的情况,如果不再校验一次,可能会产生多个实例

总结来说,就是两次校验的目的各不一样,第一层校验是为了使单例产生后锁机制失效,避免不必要的开销。第二层校验,是为了第保证存在唯一实例和延迟加载。


五、静态内部类模式


上面我们的已经介绍了懒汉模式与饿汉模式的实现,以及懒汉模式先线程安全的方案实现。从中我们可以看到,懒汉模式下,如果的能解决线程安全,单例的实现时机是比较合理的;而饿汉模式又有创建实例天然的线程安全的优势,那有没有一种取两者精华的实现方式呢?那就是下面要说到的静态内部类的实现的方式。

关于java内部类的知识,可以参考本篇文章:Java内部类

来看一下代码实现:

public class StaticInnerClassSingleton {

    private static class SingletonInnerClass{
        private static StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton(){}

    public static StaticInnerClassSingleton getInstance(){
        return SingletonInnerClass.INSTANCE;
    }
}

采用静态内部类实现,能到达一以下两点:

1、外部类加载时候并不会加载内部类,只会当调用getInstance()方法的时候才会,也就是的使用的时候才进行加载,这点跟懒汉模式的一致。

2、内部类加载的时候,该单例类的实例化是线程安全的,这点跟饿汉模式相一致。


六、枚举类


饿汉式以及懒汉式中的双重检查式、静态内部类式都无法避免被反序列化和反射生成多个实例。而枚举方式实现的单例模式不仅能避免多线程同步的问题,也可以防止反序列化和反射的破坏。

更深入的论证,可参考你知道吗?枚举单例模式是世界上最好的单例模式!!!一文。

而关于反序列化破坏单例特性,之前关于序列化的文章Java序列化也有提及,可移步阅读

public enum EnumSingleton {
    INSTANCE;
}

枚举单例模式具有以下三个优点:

  • 写法简洁,代码短小精悍。
  • 线程安全。
  • 防止反序列化和反射的破坏。

单例模式

上一篇:IMO 2021 第 1 题拓展问题的两个极值的编程求解


下一篇:v-bind动态绑定style