深度解析 Java 单例模式

前言

大家好这里是程序员阿亮,今天来给大家讲解一下Java的单例模式
在软件工程中,单例模式(Singleton Pattern) 是最简单也是最常用的设计模式之一。它属于创建型模式,核心目的在于确保一个类在整个系统中只有一个实例,并提供一个全局访问点。

像我们Spring的Bean默认就是Singleton,也就是单例

我们在项目中的很多复用类也是使用的单例,当然这种情况需要保证无状态

一、为什么要保证单例

在某些场景下,实例化多个对象会造成资源浪费或逻辑错误。常见应用场景包括:

  1. 资源共享: 数据库连接池、线程池、配置读取类(Config)。

  2. 控制中心: Windows 的任务管理器、网站的计数器。

  3. 设备驱动: 打印机后台处理服务。

二、单例模式的实现方式(由浅入深)

1.饿汉模式:Eager Initialization

饿汉模式指的是:我们饿的不行,一有机会就立刻吃掉,绝对不等到要用到的时候再吃!

也就是说当我们的类执行了的类加载的初始化(初始化是懒加载),这个单例就会被加载,不一定是使用到的时候才被加载。

java 复制代码
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

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

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  • 优点: 线程安全(由 ClassLoader 保证),写法简单。

  • 缺点: 不支持延迟加载。如果该类一直没被使用,会浪费内存资源。

2.懒汉式(Lazy Initialization)------ 线程不安全

这种模式就是当我们使用到的时候再去加载,但是在并发情况下,由于指令重排、并发访问等就会出现线程安全问题。

java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 致命缺陷: 多线程不安全。如果两个线程同时进入 if (instance == null),会创建两个实例。

三、双重检验锁定(Double-Checked Locking, DCL)------ 推荐

java 复制代码
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?

instance = new Singleton(); 这行代码在 JVM 中其实分为三步:

  1. 分配内存空间。

  2. 初始化对象。

  3. 将 instance 指向分配的内存地址。

由于 指令重排序 ,JVM 可能会执行 1 -> 3 -> 2。此时,线程 A 执行了 3 但还没执行 2,线程 B 判断 instance != null 直接取走了一个尚未初始化完成的对象,导致程序崩溃。volatile 禁用了指令重排序,确保了可见性和有序性。

四、静态内部类(Static Inner Class)------ 推荐

java 复制代码
public class Singleton {
    private Singleton() {}

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

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

这里大家可能会有疑问就是为什么基于静态内部类的单例可以被延迟加载,也就是当我们getInstance的时候再加载对象,而我们的饿汉模式只要外部类被初始化(new了对象,其他静态字段被访问)就会被加载,其原因就是:

外部类初始化与静态内部类初始化时分开的,只有我们去访问了静态内部类才会进行初始化。

  • 原理: 只有在显式调用 getInstance() 时,才会装载内部类 SingletonHolder,从而实例化 INSTANCE。这是目前最常用的优雅实现。

五、基于枚举(Enum Singleton)------ 最安全

java 复制代码
public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务逻辑
    }
}

为什么我们的枚举是天然单例呢?

Show me Code!

java 复制代码
public final class EnumSingleton extends Enum<EnumSingleton> {
    // 1. 枚举常量变成了 public static final 字段
    public static final EnumSingleton INSTANCE;
    
    // 2. 编译器自动生成的用于 values() 方法的数组
    private static final EnumSingleton[] $VALUES;

    // 3. 构造函数是 private 的,且强制调用父类 Enum 的构造器
    private EnumSingleton(String name, int ordinal) {
        super(name, ordinal);
    }

    // 4. 静态代码块 (<clinit>) 负责初始化实例
    static {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }

    public void doSomething() {
        System.out.println("Hello");
    }

    // 5. 自动生成的 valueOf 方法,用于根据名称获取实例
    public static EnumSingleton valueOf(String name) {
        return (EnumSingleton)Enum.valueOf(EnumSingleton.class, name);
    }

    // 6. 自动生成的 values 方法
    public static EnumSingleton[] values() {
        return (EnumSingleton[])$VALUES.clone();
    }
}

会发现我们的枚举定义的常量本质上就是static final字段,那么就只会在初始化的时候赋值,那么这个过程就天然的线程安全,底层是synchronized
并且我们在反射、序列化等情况也不会导致单例破坏!

当我们使用反射去创建一个新的枚举实例,会直接报错,不允许创建,这是JVM底层的机制。

在序列化时,

  1. 序列化时:只保存了枚举类的名称和常量名("SerialSingleton", "INSTANCE")。
  2. 反序列化时:读取到名字,调用 SerialSingleton.valueOf("INSTANCE")
  3. valueOf 方法:直接返回类加载时已经创建好的静态常量 INSTANCE
  4. 结果:内存中永远只有那一个对象。

六、单例模式的"天敌":如何破坏单例?

即使你写了私有构造函数,依然有两种方式可以破坏单例:

1. 反射攻击(Reflection)

java 复制代码
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 暴力访问私有构造器
Singleton s1 = constructor.newInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // false
  • 防御: 在构造函数中判断,如果已存在实例则抛出异常;或者使用枚举

2. 序列化破坏

当单例对象被写入磁盘再读取回来时,会创建一个新的实例。

  • 防御: 在类中添加 readResolve() 方法。
java 复制代码
protected Object readResolve() {
    return getInstance(); // 返回已有的单例
}

总结:该选哪种?

|---------------|------|------|----|---------|
| 实现方式 | 延迟加载 | 线程安全 | 性能 | 防反射/序列化 |
| 饿汉式 | 否 | 是 | 高 | 否 |
| 懒汉式 | 是 | 否 | 中 | 否 |
| DCL(双重检查) | 是 | 是 | 高 | 否 |
| 静态内部类 | 是 | 是 | 高 | 否 |
| 枚举 | 否 | 是 | 高 | |

工程实践建议:

  1. 首选静态内部类: 追求延迟加载且代码优雅。

  2. 首选枚举: 追求绝对的安全性(防止被反射破坏)。

  3. 如果涉及多线程复杂环境: 务必使用 DCL 且带上 volatile。

相关推荐
NGC_66111 小时前
G1收集器
java·开发语言·jvm
森林里的程序猿猿2 小时前
垃圾收集器ParNew&CMS与底层标记三色标记算法
java·jvm·算法
t_hj2 小时前
腾讯QClaw深度试用:一句话创建专业级网络爬虫
开发语言·python
老毛肚2 小时前
八股框架篇
java·开发语言
大黄说说2 小时前
Rust 入门到实战:构建安全、高性能的下一代系统
开发语言·安全·rust
毅炼2 小时前
Spring 总结(1)
java·开发语言·spring
jing-ya2 小时前
day 55 图论part7
java·数据结构·算法·图论
阿蒙Amon2 小时前
C#常用类库-详解NModbus4
开发语言·c#