单例模式

175 阅读6分钟

概念:确保在整个程序的声明周期,一个类只能有一个实例,并提供一个全局访问点。

特点:1、只有一个实例。

2、全局访问点,便于管理共享资源。

3、防止多个实例导致的数据不一致问题

场景:

  1. 配置管理:如读取配置文件的类,整个系统共享一份配置。
  2. 日志管理:日志写入器通常使用单例,确保日志文件不会被多个实例同时导入。
  3. 数据库连接池:确保整个系统使用相同的数据库连接池
  4. 线程池:避免重复创建线程池,提升性能
  5. 缓存管理:共享缓存实例,提升访问效率

注意:用户信息是不适用于单例模式的。

用户信息特性:

  1. 多实例要求。
    1. 大多数应用中,系统需要同时支持多个用户,每个用户的信息时独立的。
    2. 单例模式只能保证全局唯一实例,所以不能满足多用户需求
  1. 状态变化频繁
    1. 用户信息是动态的,可能会频繁更新。
    2. 如果多个用户信息用单例存储。多个用户共享一个实例,容易造成信息混乱或者覆盖
  1. 声明周期管理
    1. 用户信息的声明周期通常与会话或者请求相关。用户退出时其信息可能会被销毁。而单例模式的生命周期通常时整个程序的声明周期。这就与用户信息的续期不匹配

所以将用户信息放到ThreadLocal中时比较适合的。ThreadLocal 可以为每个线程提供独立的变量副本,这样每个线程都有自己独立的用户信息实例。

private static final ThreadLocal userThreadLocal = new ThreadLocal<>(); 是一个 类变量,它在类加载时就会初始化并创建。

使用当前ThreadLocal 实例作为键,但是这个实例不是只有一份吗 ?

虽然 ThreadLocal 实例只有一份,但它用于不同线程中的独立存储。

ThreadLocal和ThreadLocalMap 的关系

ThreadLocal 是一个工具类,供开发者使用,用于设置和获取与当前线程相关联的数据

  • ThreadLocalMapThreadLocal 的静态内部类,是实现线程隔离存储的核心数据结构。
  • 每个线程内部维护一个 ThreadLocalMap 实例,这个实例存储所有和当前线程相关联的 ThreadLocal 数据。

饿汉式


代码示例

public class EhanSingleton {
    private static EhanSingleton instance =new EhanSingleton();
    private EhanSingleton(){

    }
    public static EhanSingleton getInstance(){
        return instance;
    }

}

特点

  1. 线程安全,虽然没有加锁。 根据Java规范,类的静态变量会在类初始化阶段由JVM保证线程安全,且类加载和初始化只会进行一次
  2. 类加载时候就创建了实例,可能会浪费资源

问:单例,为什么会浪费资源,即使实例已经创建好了没有用,但是也是只有一个实例啊

答:类加载时就初始化,无论实例是否被真正的使用。可能导致提前占用资源。而这些资源在整个程序的声明周期中没有使用过

懒汉式

代码示例:

public class LanHanSingleton {

    private static LanHanSingleton singleton;

    private LanHanSingleton (){

    }
    public static LanHanSingleton getInstance(){
        if(singleton==null){
            // 仅当第一次调用时创建实例
            singleton=new LanHanSingleton();
        }
        return singleton;
    }

}

特点:在需要时才创建实例,线程不安全

问:getInstance()为什么不能用private?

答:单例模式的核心就是一个类只有一个实例并提供一个全局访问点来获取这个实例,如果设置成private,外部就无法访问。

线程安全的懒汉式

代码示例

public class LanHanSingleton {

    private static LanHanSingleton singleton;

    private LanHanSingleton (){

    }
    public static synchronized LanHanSingleton getInstance(){
        if(singleton==null){
            // 仅当第一次调用时创建实例
            singleton=new LanHanSingleton();
        }
        return singleton;
    }

}

特点:

  1. 加了同步锁,线程安全
  2. 但是也会降低性能,每次获取实例都需要加锁。

双重检查锁

代码示例

public class DoubleLanHanSingleton {


    private static volatile DoubleLanHanSingleton instance;

    private DoubleLanHanSingleton() {
    }

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

}

特点

  1. 延迟加载,线程安全
  2. 性能比较高,仅第一次需要加锁

问:为什么判断两次instance?

答:主要是为了避免不必要的同步开销。第一次判断 主要用于避免不必要的同步。只有当instance为null时,才进行同步块,否则直接返回已经创建好的实例。避免每次调用getInstance时都进行同步,提高性能。第二次是为了保证线程安全, 只有第一个进入同步块的线程会创建实例,而其他线程会看到已创建的实例 。 由于多个线程可能会在第一次检查时都看到 instance == null,因此在同步块内需要再次检查 instance 是否仍然为 null,以确保只有一个线程会创建实例。

静态内部类

代码示例

public class StaticSingleton {

    private StaticSingleton(){}
    // 静态内部类,延迟加载
    private static class SingletonHolder{
        private static final StaticSingleton instance = new StaticSingleton();
    }

    public static final StaticSingleton getInstance(){
        return SingletonHolder.instance;
    }
    
}

特点

  1. 延迟加载
  2. 线程安全

问: SingletonHolder 为什么在使用的时候才会被加载 ?

答:1、Singleton 类本身被加载

  • 当调用 Singleton.getInstance() 时,Singleton 类的字节码会被加载到 JVM 中,但此时并不会加载 SingletonHolder 类。
  • 因为 SingletonHolderSingleton 的静态内部类,它的加载是独立的

2、SingletonHolder 的加载时机

  • 只有在真正访问 SingletonHolder.INSTANCE 时,SingletonHolder 类才会被加载到 JVM 中。
  • 加载时会对静态字段 INSTANCE 进行初始化。
  • 这种机制是 Java 的类加载按需原则的体现。

3、 静态字段的初始化

  • SingletonHolder 类被加载时,其静态变量 INSTANCE 会被初始化。
  • 静态变量的初始化过程是线程安全的,由 JVM 保证。

枚举

代码示例

public enum EnumSingleton {

    INSTANCE;

    public void doSomething() {

    }

}

特点

使用枚举最大的特点就是防止反射破坏单例 。反射破坏反序列化破坏

通过反射可以调用私有构造函数创建新实例,破坏单例模式。例如:

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

    private Singleton() {}

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

使用反射可以绕过私有构造函数:

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newInstance = constructor.newInstance();

但枚举的构造方法是由 JVM 强制为私有的,并且反射无法调用枚举的构造方法,会抛出 java.lang.NoSuchMethodException

普通单例可能被反序列化破坏,例如:

Singleton singleton1 = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
oos.writeObject(singleton1);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton singleton2 = (Singleton) ois.readObject();

上述代码可能导致 singleton1 != singleton2

枚举类型天然防止反序列化攻击:

  • Java 的枚举类在反序列化时会使用其内置的机制,保证枚举值不会被反序列化为新的对象,而是返回现有的枚举实例。