浅谈单例模式

Posted by Don on October 10, 2019

在日常开发中,单例模式是我们计较常用的设计模式之一,今天,我们就来看一下单例模式的几种实现。

定义

确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例

单例模式是一种对象创建型模式。

单例模式3要素

  1. 声明一个类型为自身的静态私有成员变量
  2. 声明一个公有的静态工厂方法,返回唯一实例
  3. 将构造函数的可见性修改为private

饿汉式单例

饿汉式单例的实例在类装载时进行创建。由于是在类装时候创建, 所以能够保证线程安全,而且反应时间和调用速度要优于懒汉式单例。

实现代码:

public class EagerSingleton {
    private static EagerSingleton eagerSingleton = new EagerSingleton();
    private EagerSingleton() {

    }

    private static EagerSingleton getInstance() {
        return eagerSingleton;
    }
}

饿汉式单例存在2个问题,如下所示:

  1. 类装载时该对象就被创建,如果此对象没有被调用,那么会造成资源的浪费
  2. 对象创建时会调用该类的构造方法,如果构造方法中有过多处理会导致该类加载时间比较长

懒汉式单例与双重校验锁(Double Check Lock)

懒汉式单例的实例在第一次被引用时初始化,实现了延迟加载。

懒汉式单例存在一个很严重的问题:线程不安全。如果在高并发、多线程环境下实现懒汉式单例的话,可能会创建多个实例对象,为了解决这个问题,一般在实现懒汉式单例时会加同步锁,代码如下:

public class LazySingleton {
    private static LazySingleton lazySingleton;
    private LazySingleton() {

    }

    private static LazySingleton getInstance() {
        //第一重判断,避免非必要加锁,判断实例是否存在,不存在则加锁创建
        if (lazySingleton == null) {
            //上锁,某一时刻只允许一个线程访问
            synchronized (LazySingleton.class) {
                //第二重判断
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }

        }

        return lazySingleton;
    }
}

有的同学可能会说直接使用synchronized修改getInstance()方法不就可以了吗,不用这么麻烦。是,直接修饰方法确实可以达到效果,但是众所周知,synchronized效率低,而且只有当其修饰的代码块执行完成后才会释放锁,因此synchronized修饰的代码能少则少。

那么,使用了DCL是否就能百分百确保线程安全了吗?答案是否定的,因为jvm存在乱序执行功能,分析如下:

lazySingleton = new LazySingleton();

执行上面创建语句时,jvm分3步:
1. 在堆内存开辟内存空间
2. 在堆内存中实例化LazySingleton里面的各个参数
3. 把对象指向堆内存空间

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,lazySingleton 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。那么这个问题如何解决呢?在JDK1.6及以后,只要使用volatile修饰该参数即可,因为volatile确保顺序正常执行。如下所示:

private volatile static LazySingleton  lazySingleton;

静态内部类单例

外部类加载时并不需要立即加载内部类,内部类(不论是静态内部类还是非静态内部类)都是在第一次使用时才会被加载,内部类不被加载则不去初始化INSTANCE,因此静态内部类单例的实例在第一次被引用时初始化。
实现了延迟加载,并且线程安全。
那么静态内部类是如何保证线程安全的呢?可查看此篇文章JAVA类加载过程&主动引用和被动引用
代码实现:

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {

    }

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

    static class SingletonHolder{
        static StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
}

静态内部类有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数。

枚举单例

以上几种单例模式都有一个通病,就是无法防止反射机制的漏洞,从而无法保证对象的唯一性。那如何解决此问题呢?枚举单例就为此而生,枚举单例代码如下:

public enum EnumSingleton{
  INSTANCE;
}

枚举单例也有一个问题,那就是无法实现懒加载(延迟加载)。所以,我们创建单例时,请结合自己的使用场景选择不同的方式。

总结

单例模式优点:

  1. 可以确保对象的唯一性,并提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以可以严格控制用户怎么访问它以及何时访问它

  2. 可以节约系统资源,因为在系统中只存在一个对象。对于一些需要频繁创建和销毁的对象,使用单例可以提高系统的性能

单例模式缺点:

  1. 由于单例模式中没有抽象层,因此,单例类的扩展有很大困难

  2. 单例类的职责过重,在一定程度上违背了单一职责原则。 因为单例类即提供了业务方法,也提供了创建对象的方法。