设计模式-创建型模式-单例模式

27 May 2021

Reading time ~34 minutes

意图

单例模式是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

image-20210527153510545

问题

单例模式同时解决了两个问题,所以违反了单一职责原则:

  1. 保证一个类只有一个实例。为什么会有人想要控制一个类所拥有的实例数量?最常见的原因是控制某些共享资源的访问权限

    它的运作方式是这样的:如果你创建了一个对象,同时过一会儿后你决定再创建一个新对象,此时你会获得之前已创建的对象,而不是一个新对象。

    注意,普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。

    image-20210527153815627

  2. 为该实例提供一个全局访问节点。还记得你用过的那些存储重要对象的全局变量吗?他们在使用上十分方便,但同时也非常不安全,因为任何代码都有可能覆盖掉那些变量的内容,从而引发程序崩溃

    和全局变量一样,单例模式也允许在程序的任何地方访问特定对象。但是它可以保护该实例不被其他代码覆盖。

    还有一点:你不会希望解决同一个问题的代码分散在程序各处的。因此更好的方式是将其放在同一个类中,特别是当其他代码已经依赖这个类时更应该如此。

如今,单例模式已经变得非常流行,以至于人们会将只解决上文描述中任意一个问题的东西称为单例。

解决方案

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有,防止其他对象使用单例类的new运算符
  • 新建一个静态构造方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总会返回相同的对象。

真实世界类比

政府是单例模式的一个很好的示例。一个国家只有一个官方政府。不管组成政府的每个人的身份是什么,“某政府”这一称谓总是鉴别那些掌权者的全局访问节点。

单例模式结构

image-20210527154635056

单例模式适合应用场景

如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。

单例模式禁止通过特殊构造方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象,但如果该对象已经被创建,则返回已有的对象。

如果你需要更加严格地控制全局变量,可以使用单例模式。

单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外,无法通过任何方式替换缓存的实例。

请注意,你可以随时调整限制并设定生成单例实例的数量,只需修改获取实例方法,即getInstance中的代码即可实现。

实现方式

  1. 在类中添加一个私有静态成员变量用于保存单例实例
  2. 声明一个公有静态构建方法用于获取单例实例
  3. 在静态方法中实现“延迟初始化”。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次调用时都返回该实例
  4. 将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用
  5. 检查客户端代码,将对单例的构造函数的调用替换为对静态构建方法的调用

单例模式优缺点

优点:

  • 可以保证一个类只有一个实例
  • 获得了一个指向该实例的全局访问节点
  • 仅在首次请求实例对象时对其进行初始化

缺点:

  • 违反了单一职责原则。该模式同时解决了两个问题
  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等
  • 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象
  • 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例模式的构造函数是私有的,而且觉得绝大部分语言无法重写静态方法,所有你需要想出仔细考虑单例模式的方法。要么干脆不编写测试代码,或者不使用单例模式

与其他模式的关系

  • 外观模式类通常可以转换为单例模式类,因为在大部分情况下一个外观对象就足够了。
  • 如果你能将对象的所有共享状态简化为一个享元对象,那么享元模式和单例类似了。但是这两个模式有两个根本性的不同:
    1. 只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同
    2. 单例对象可以是可变的,享元对象是不可变的
  • 抽象工厂、生成器和原型都可以用单例模式来实现。

以上摘自REFACTORING GURU

代码示例:

饿汉:

/**
 * 饿汉单例模式
 * 线程安全
 * 但是在类加载的时候就会实例化对象,如果该对象没有被使用,内存就浪费了
 *
 */
public class HungrySingleton {
    private final static HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton(){

    }

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

懒汉:

/**
 * 简单懒汉单例模式
 * 线程不安全
 * 在多线程的场景下,两个线程同时访问,可能会多次实例化
 *
 */
public class LazySingleton {
    private static LazySingleton instance = null;

    private LazySingleton(){

    }

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

同步方法懒汉:

/**
 * 同步方法懒汉单例模式
 * 线程安全
 * 其实只需要执行一次就可以实例化,每次都同步效率较低
 *
 */
public class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton instance = null;

    private SynchronizedLazySingleton(){

    }

    public static synchronized SynchronizedLazySingleton getInstance(){
        if(instance == null){
            instance = new SynchronizedLazySingleton();
        }
        return instance;
    }
}

双重锁检查懒汉:

/**
 * 双重检查懒汉单例模式
 * 线程安全
 * 同步代码块效率较高,第二个if判断是为了避免两个线程同时调用可能出现多次实例化
 *
 */
public class DoubleCheckLazySingleton {
    private static volatile DoubleCheckLazySingleton instance = null;

    private static Object lock = new Object();

    private DoubleCheckLazySingleton(){

    }

    public static DoubleCheckLazySingleton getInstance(){
        /**
         * The local variable result seems unnecessary. But, it’s there to improve the performance of our code.
         * In cases where the instance is already initialized (most of the time), the volatile field is only
         * accessed once (due to “return result;” instead of “return instance;”). This can improve the
         * method’s overall performance by as much as 25 percent.
         */
        DoubleCheckLazySingleton result = instance;
        if(result == null){
            synchronized (lock){
                if(result == null){
                    result = new DoubleCheckLazySingleton();
                }
            }
        }
        return result;
    }
}

静态内部类懒汉:

/**
 * 静态内部类懒汉单例模式
 * 线程安全
 * 在类被装载时不会立即实例化,而是在需要实例化时调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化
 * 同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次
 *
 */
public class StaticInnerClassLazySingleton {
    private StaticInnerClassLazySingleton(){

    }

    private static class SingletonHelper{
        private static final StaticInnerClassLazySingleton INSTANCE = new StaticInnerClassLazySingleton();
    }

    public StaticInnerClassLazySingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}

枚举:

/**
 * 枚举单例模式
 * 线程安全
 * 因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块
 * 同时避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现
 *
 * 但是不灵活,不能懒加载
 *
 */
public enum  EnumSingleton {
    INSTANCE;

    public static void doSomething(){

    }
}

序列化:

/**
 * 有时在分布式系统中,我们需要在Singleton类中实现Serializable接口,
 * 以便我们可以将其状态存储在文件系统中,并在以后的时间点检索它
 *
 * 但是当我们反序列化它时,它将创建一个新的类实例
 * 解决方法是加上 readResolve() 实现
 *
 */
public class SerializedSingleton implements Serializable {
    private static final long serialVersionUID = -1687515539276464560L;

    private SerializedSingleton(){
    }

    private static class SingletonHelper{
        private static final SerializedSingleton INSTANCE = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }

    protected Object readResolve(){
        return getInstance();
    }
}


Design PatternsProfessional Knowledge Share Tweet +1