Bootstrap

23种设计模式之单例模式

1. 简介

单例模式(Singleton Pattern)是一种创建型设计模式。它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。就好像在一个软件系统中,对于某些特定的资源或者对象,我们只希望有一个存在,比如系统的配置信息对象、数据库连接池等。

2. 代码

2.1 懒汉式

LazySingleton

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {}

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

Run(用来查是不是同一个对象)

public class Run implements Runnable{
    @Override
    public void run() {
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + " " + instance);
    }
}

Test

public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Run());
        Thread t2 = new Thread(new Run());
        t1.start();
        t2.start();
        System.out.println("end");
    }
}

运行结果:

end
Thread-1 yxz.singleton.LazySingleton@3b1ecb6f
Thread-0 yxz.singleton.LazySingleton@3b1ecb6f

存在问题:线程不安全,这里的main方法,多次执行,可能出现不同的对象,
原因:我们知道,代码是一行一行执行的,线程间是并发执行的。当我们执行lazySingletonlazySingleton == null时,其他线程可能也执行到这一行,随后都判断为true,这造成创建了两个。
解决方式:在方法上加入synchronized关键字就好。

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

这里的instance变量在开始的时候没有被初始化。只有当getInstance方法第一次被调用,并且instancenull的时候,才会创建LazySingleton类的实例。

2.2 双重检查

DoubleCheckSingleton

public class DoubleCheckSingleton {
    private static DoubleCheckSingleton doubleCheckSingleton;
    private DoubleCheckSingleton(){}

    public static DoubleCheckSingleton getInstance(){
        if(doubleCheckSingleton == null){
            synchronized(DoubleCheckSingleton.class){
                if(doubleCheckSingleton == null){
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }
}
  • 双重检查锁定是为了减少synchronized关键字带来的性能开销。首先检查instance是否为null,如果是,才进入synchronized块。
  • synchronized块中再次检查instance是否为null,这是因为可能有多个线程同时通过了第一次检查,但是只有一个线程能够进入synchronized块创建实例。

2.3 静态内部类

StaticInnerClassSingleton

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    private StaticInnerClassSingleton(){
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
}
  • Singleton类被加载时,它的内部类InnerClass不会被立即加载。只有当getInstance方法被调用时,InnerClass类才会被加载,并且初始化staticInnerClassSingleton 变量。
  • 这种方式利用了类加载机制来保证线程安全,因为类加载过程是由 JVM 保证线程安全的。

2.4 饿汉式

HungrySingleton

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){}
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
  • 在这个例子中,hungrySingletonHungrySingleton 类的一个静态成员变量。因为它是static的,所以在类加载的时候就会被初始化,并且不能被修改。
  • 构造方法被声明为private,这意味着其他类无法通过new关键字来创建HungrySingleton 类的实例。
  • getInstance方法是一个静态方法,外部类可以通过HungrySingleton.getInstance()来获取这个唯一的实例。

2.5 枚举(推荐)

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance()
    {
        return INSTANCE;
    }
}

破坏单例的方式:序列化和反序列化,反射
枚举实现单例模式天然地避免了这些隐患:

  • 反射方面Java 的反射机制在尝试调用枚举类型的构造函数来创建新实例时,会抛出 java.lang.NoSuchMethodException异常,因为 Java 不允许通过反射去创建枚举类型的实例,从根源上杜绝了反射破坏单例的可能性。
  • 序列化方面:枚举类型在序列化和反序列化过程中,Java 会保证始终只有最初的那一个实例存在。即无论进行多少次序列化和反序列化操作,得到的都是同一个枚举实例,这是因为枚举本身的序列化机制是由 Java 内部特殊处理的,它依靠的是枚举的 name 属性(在上述例子中就是 INSTANCE 这个名称)来进行序列化还原,而不是像普通对象那样基于对象的状态进行常规的序列化流程,所以不会产生新的实例破坏单例性。

优点:

  • 简洁性:代码实现非常简洁,相较于传统的单例模式实现(如饿汉式、懒汉式等多种复杂的代码结构),利用枚举实现只需要简单定义枚举类型以及其中的实例和相关属性、方法即可,代码量少且易于理解。
  • 线程安全:由编程语言自身的机制保障了线程安全,不需要额外添加如 synchronized 关键字等复杂的线程同步机制来保证在多线程环境下只有一个实例被创建。
  • 防止破坏:能有效避免被反射和序列化反序列化等操作破坏单例的情况,从多方面保障了整个系统中只有唯一的单例实例存在,增强了单例模式应用的稳定性和可靠性。

3. 总结

单例模式的应用场景

  • 配置管理
    系统的配置信息通常是全局唯一的。例如,一个应用程序的数据库连接配置、服务器端口配置等。使用单例模式可以确保在整个应用程序中只有一份配置实例,方便统一管理和维护。如果有多个地方需要访问配置信息,都可以通过单例模式提供的全局访问点来获取。
  • 日志系统
    日志记录器通常也是单例的。在一个应用程序中,我们希望所有的日志信息都记录到同一个地方,方便查看和管理。单例模式可以保证日志记录器的唯一性,不同的模块可以通过获取这个单例的日志记录器来记录日志。
  • 线程池
    线程池是一种管理和复用线程的机制。在一个应用程序中,通常只需要一个线程池来处理各种任务。单例模式可以确保只有一个线程池实例存在,避免了资源的浪费和混乱的线程管理。

单例模式的优缺点

  • 优点
    • 资源共享:可以在整个系统中共享唯一的实例,避免了创建多个相同对象带来的资源浪费。例如,数据库连接池单例可以共享连接资源,提高资源利用率。
    • 全局访问点:提供了一个统一的全局访问点,方便在不同的模块中获取和使用这个唯一的实例。这使得代码更加简洁和易于维护。
  • 缺点
    • 违反单一职责原则:单例类可能会承担过多的职责,因为它在整个系统中是唯一的,可能会被用于多种不同的功能。这会导致单例类的代码变得复杂,难以理解和测试。
    • 单元测试困难:由于单例类的实例是全局唯一的,在进行单元测试时可能会影响其他测试用例。而且单例类的依赖性较强,很难模拟它的行为进行独立的单元测试。
;