单例模式终极指南:从入门到防破坏

目录

引言

一、什么是单例模式

二、饿汉式(最简单)

三、懒汉式(用时才创建)

[3.1 线程不安全版(错误示范)](#3.1 线程不安全版(错误示范))

[3.2 synchronized 同步方法版](#3.2 synchronized 同步方法版)

[3.3 双重检查锁 DCL(最常用)](#3.3 双重检查锁 DCL(最常用))

四、静态内部类(优雅且推荐)

五、枚举实现(最推荐)

六、破坏单例的两种方式与防范

[6.1 反射破坏](#6.1 反射破坏)

[6.2 序列化破坏](#6.2 序列化破坏)

[七、Spring 中的单例](#七、Spring 中的单例)

总结


引言

在开发中经常会有这样一类对象:整个应用只需要一个,多了反而会出问题。比如数据库连接池、线程池、配置管理类、日志对象、Spring 容器里的 Bean......如果这些对象被反复 new 出来,不仅浪费资源,还可能引发状态不一致、端口占用等一连串问题。

单例模式(Singleton Pattern)就是用来解决"保证一个类只有一个实例,并提供一个全局访问点"这个问题的。它属于创建型设计模式,也是面试和日常开发里最常打交道的模式之一。这篇文章我会从最简单的写法一路讲到生产环境最推荐的写法,并带你看看单例是怎么被"破坏"、又该怎么防范的。

一、什么是单例模式

单例模式的核心就两点:

  1. 私有化构造方法 :让外部无法通过 new 随意创建实例。
  2. 提供一个静态的全局访问方法:通过这个方法获取唯一实例。

一个最朴素的单例骨架长这样:

java

java 复制代码
public class Singleton {
    // 1. 私有构造方法
    private Singleton() {}

    // 2. 静态方法返回唯一实例
    public static Singleton getInstance() {
        // 返回唯一实例
        return ...;
    }
}

关键在于"唯一实例"到底怎么创建、什么时候创建、怎么保证线程安全,这正是下面要展开的。

二、饿汉式(最简单)

饿汉式的意思是"一上来就创建好",不管用不用,类加载时就 new 出实例。

java

java 复制代码
public class Singleton {
    // 类加载时就创建实例,线程安全
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  • 优点:实现最简单;基于类加载机制天然线程安全,没有加锁开销。
  • 缺点:如果这个实例很"重"(占用大量资源)而你又一直没用它,就造成了浪费。
  • 适用场景:实例创建开销小、或确定一定会用到的场景。

三、懒汉式(用时才创建)

懒汉式的意思是"懒",第一次用到的时候才去创建实例。

3.1 线程不安全版(错误示范)

java

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) 判断,就会各自 new 一个实例,破坏单例。所以这个写法在多线程环境下是错误的。

3.2 synchronized 同步方法版

最简单的补救是给方法加锁:

java

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

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 优点:线程安全了。
  • 缺点 :每次调用 getInstance() 都要加锁,即使实例早就创建好了,性能较差。

3.3 双重检查锁 DCL(最常用)

为了"既线程安全、又只在第一次创建时加锁",出现了双重检查锁(Double-Checked Locking):

java

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;
    }
}

两个关键点:

  1. 两次判空:外层判空是为了"已经有实例就跳过加锁"(提高性能);内层判空是为了"防止多线程同时通过外层判断后重复创建"。
  2. volatile 关键字 :new Singleton() 这条语句在底层不是原子的,会发生"分配内存 → 初始化对象 → 将引用指向内存"三步,其中后两步可能被指令重排。如果没有 volatile,另一个线程可能拿到一个"还没初始化完成"的对象。加了 volatile 可以禁止这种重排,保证内存可见性。

四、静态内部类(优雅且推荐)

利用 JVM 类加载机制实现懒加载 + 线程安全,代码更简洁:

java

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

    // 静态内部类,只有在被使用时才会加载
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
  • 懒加载 :Holder 内部类只有在 getInstance() 第一次被调用时才会被 JVM 加载,此时才创建实例。
  • 线程安全 :由 JVM 的类加载机制保证,无需任何锁和 volatile。
  • 优点:写法简洁、无锁、性能好,是除了枚举之外最推荐的写法之一。

五、枚举实现(最推荐)

《Effective Java》作者 Joshua Bloch 推荐的终极写法:

java

java 复制代码
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("单例方法被调用");
    }
}

// 使用
Singleton.INSTANCE.doSomething();
  • 线程安全:枚举由 JVM 保证只初始化一次。
  • 防反射、防序列化:枚举天然免疫后面要讲的两种破坏方式,是最安全的单例实现。
  • 唯一"缺点":如果你的单例需要继承某个类,枚举就不适用了(枚举不能继承)。除此之外,能上枚举就上枚举。

六、破坏单例的两种方式与防范

6.1 反射破坏

上面的饿汉式、懒汉式、静态内部类都靠"私有构造方法"来阻止外部 new,但反射可以绕过这个限制:

java

java 复制代码
public static void main(String[] args) throws Exception {
    Singleton s1 = Singleton.getInstance();

    // 通过反射拿到私有构造方法
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true); // 暴力破解私有权限
    Singleton s2 = constructor.newInstance();

    System.out.println(s1 == s2); // false!单例被破坏
}

防范:在构造方法里加判断,如果实例已存在则抛异常:

java

java 复制代码
private Singleton() {
    if (instance != null) {
        throw new RuntimeException("单例已存在,禁止反射创建");
    }
}

不过最省心的还是直接用枚举 ,反射也拿它没办法(Constructor.newInstance 对枚举会直接抛异常)。

6.2 序列化破坏

如果一个单例类实现了 Serializable 接口,那么"序列化再反序列化"也会得到一个新对象:

java

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

    private Singleton() {}

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

// 序列化 → 反序列化后,得到的是新实例

防范 :加一个 readResolve() 方法,反序列化时返回已有的唯一实例:

java

java 复制代码
// 反序列化时会被调用,返回单例本身
private Object readResolve() {
    return INSTANCE;
}

七、Spring 中的单例

如果你用 Spring,那"单例"几乎无处不在。Spring 容器管理的 Bean,**默认作用域就是单例(singleton)**​:

java

java 复制代码
@Service  // 默认就是单例
public class UserService {
    // ...
}

同一个 Bean 在整个容器里只有一个实例,被所有注入它的地方共享。这也是为什么我们在 Service 里不要定义有状态的可变成员变量(比如用成员变量存请求相关的数据),否则多线程并发访问同一单例 Bean 时会产生数据串扰问题。

java

java 复制代码
@Service
public class UserService {
    // ❌ 不要这样:多线程共享同一个 Bean,会串数据
    private String currentUserName;

    public void doSomething(String name) {
        this.currentUserName = name; // 并发下会被其他线程覆盖
    }
}

总结

文章我们梳理了单例模式的完整知识:

  1. 饿汉式:最简单、天然线程安全,但可能浪费资源。
  2. 懒汉式 :用时才创建,但要注意线程安全------普通写法不安全,synchronized 方法性能差,双重检查锁 DCL (配合 volatile)最常用。
  3. 静态内部类:借助 JVM 类加载机制实现懒加载 + 线程安全,优雅无锁。
  4. 枚举:最安全、最推荐,天然防反射、防序列化,能上就上。
  5. 破坏与防范 :反射和序列化都能破坏单例,可通过构造方法守卫和 readResolve() 防范,枚举天然免疫。
  6. Spring:容器中的 Bean 默认就是单例,因此要避免在单例 Bean 里持有可变状态。

一句话总结:日常开发优先用枚举;不能枚举就用静态内部类或双重检查锁;同时牢记单例"无状态"的使用原则。

顺带提一下接下来六篇我应该都会更新设计模式相关的内容,之后我会好好打磨一个博客项目给大家分享OVO

相关推荐
博风6 小时前
9、设计模式应用举例:责任链模式:计算阶乘
设计模式·责任链模式
电子IT笔记8 小时前
人走了,记录不能“空白”: MES×HR 离职人员下拉回显实战
设计模式
博风1 天前
8、设计模式应用举例:适配器模式:Iterator接口与Enumeration接口
设计模式·适配器模式
高晶1 天前
ARM 嵌入式 Linux 系统 U-Boot 开发与移植实战M
设计模式·程序员
geovindu1 天前
rust: Chain of Responsibility Pattern
开发语言·后端·设计模式·rust·责任链模式·行为模式
拆房老料1 天前
【第四部分:Agentic 智能体设计模式】15.为什么 Agent 也需要设计模式
人工智能·设计模式·ai·ai-native
geovindu1 天前
rust: Chain of Responsibility Pattern (续)
开发语言·后端·设计模式·rust·责任链模式·行为模式
Lost of 程序猿1 天前
责任链模式实战:审批流里“层层把关“的艺术
后端·设计模式·asp.net·责任链模式
希洛兔2 天前
状态模式详解:让对象随内部状态自动改变行为
java·设计模式·状态模式