设计模式——单例模式(饿汉式,懒汉式等)

发布于:2024-12-19 ⋅ 阅读:(9) ⋅ 点赞:(0)

设计模式——单例模式(饿汉式,懒汉式等)

概念

单例模式(Singleton Pattern) 是一种创建型设计模式,它的目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例

核心要点

  • 唯一性
    类只有一个实例,避免重复创建多个对象导致资源浪费或状态不一致。
  • 全局访问点
    提供一个全局方法,方便访问唯一实例。

实现

基础要点

根据单例模式的特点,要求实现单例模式必须要构造器私有化,防止外界创建对象

饿汉式

顾名思义,该方式就是直接在类加载时就初始化单例实例

示例代码如下:

public class Singleton {
    // 静态变量在类加载时初始化
    private static final Singleton INSTANCE = new Singleton();

    // 私有化构造器,防止外部实例化
    private Singleton() {}

    // 提供全局访问点
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  • 优点:实现简单,线程安全。
  • 缺点:即使未使用该实例,也会初始化,占用内存。

懒汉式

也是顾名思义,就是“懒”,即在类加载时并不会立即创建实例,而是在外界需要时才会创建实例

示例代码如下:

public class Singleton {
    // 静态变量,延迟初始化
    private static Singleton instance;

    // 私有化构造器
    private Singleton() {}

    // 提供全局访问点
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 优点:按需创建,节省资源。
  • 缺点:在多线程环境下存在安全问题(多个线程可能同时创建实例)

懒汉式(线程安全,双重检查锁定)

由于在高并发情况下,一般的懒汉式方案可能会出现线程安全问题,所以需要对其进行改造

改造后如下:

public class Singleton {
    // 使用 volatile 确保可见性和禁止指令重排序
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 优点:线程安全,性能较高
  • 缺点:实现复杂

这里补充一下 volatile 关键字:

volatile 是 Java 提供的一个关键字,用于修饰变量,确保变量在多线程环境中的可见性禁止指令重排序。这些特性是为了解决并发编程中的常见问题,如线程间数据不一致和由于优化导致的潜在问题。

  • 可见性

在多线程环境中,每个线程都有自己的工作内存(CPU 缓存),线程可能会将变量的副本缓存在自己的工作内存中,导致一个线程对变量的修改对另一个线程不可见。而使用volatile 修饰的变量,所有线程对其的读写操作直接发生在主内存中。当一个线程修改了 volatile 变量的值,其他线程能够立即看到最新值。

  • 禁止指令重排序

为了优化性能,CPU 和编译器可能会对指令进行重排序(Instruction Reordering)。在单线程环境下,这种重排序不会改变程序的执行结果,但在多线程环境下,可能导致意想不到的问题,所以也得注意一下。volatile 通过加入内存屏障(Memory Barrier),确保指令在多线程环境中按程序的逻辑顺序执行。它可以防止变量的赋值操作被重排序到不合适的位置。
这里举一个例子:
示例代码:

class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                   // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {           // 第二次检查
                    instance = new Singleton();   // 问题出在这里
                }
            }
        }
        return instance;
    }
}

这里看到 instance 没有被 volatile修饰,那么就可能会出现问题:

instance = new Singleton() 是一个分解过程,可能会被重排序成以下步骤:

  1. 分配内存空间。
  2. 将内存地址赋值给 instance
  3. 调用构造函数初始化对象。

如果发生了重排序,步骤可能变成:

  • 先执行第 2 步,再执行第 3 步

此时,另一个线程读取 instance 时,可能得到一个“未初始化完全”的对象。

静态内部类实现

利用类加载机制实现线程安全,推荐使用

public class Singleton {
    // 静态内部类
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {}

    // 通过静态内部类返回实例
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

优点

  • 线程安全
  • 延迟加载
  • 实现简单

简单解读一下:

在外部类加载时,并不会主动加载内部类,而只有当外界主动使用内部类时,内部类才会被类加载器加载。借助这一特性可以实现延迟加载。并且,当类加载加载内部类时,由 JVM 保证线程的安全性,这样可以实现线程安全。总的来看,操作还是十分简单的。

使用枚举实现

使用 枚举(Enum) 是单例模式的一种推荐实现方式,尤其在 Java 环境中。枚举不仅能实现线程安全的单例,还能天然防止反射攻击序列化漏洞,是最简洁且安全的单例实现方式。

示例代码:

public enum Singleton {
    INSTANCE; // 枚举类型的唯一实例

    // 示例方法
    public void doSomething() {
        System.out.println("Executing some logic in Singleton");
    }
}

优点

  • 简洁
  • 天然线程安全
  • 防止反射和序列化攻击

简单解读一下:

在 Java 中,每个枚举类型都是通过 java.lang.Enum 类实现的。枚举类的实例化是由 JVM 保证的,并且其类加载机制确保了线程安全性。每个枚举常量(如 INSTANCE)在类加载时就会被初始化(饿汉式),且只会初始化一次。
通过反射可以破坏普通单例模式,调用私有构造器来创建新的实例。但枚举类型的构造器在 Java 中是隐式的,JVM 不允许通过反射调用枚举类型的构造器,因此无法创建多个实例。
普通单例在序列化和反序列化时可能生成多个实例(通过 ObjectInputStream)。枚举类型的序列化由 JVM 自动处理,枚举单例在序列化时只会返回相同的枚举实例

总结

多种实现方案的比较如下:

image-20241216205221031