2、单例模式原理:单例模式的定义、适用场景、饿汉式与懒汉式的核心区别

单例模式,说白了就是保证一个类在整个系统里只有一个实例。你想想看,有些对象我们真的不需要创建多个——比如配置管理器、线程池、数据库连接池。如果每个地方都 new 一个,内存浪费不说,数据还可能不一致。

我个人习惯把单例模式看作「全局唯一看门人」。它自己管着自己的实例,别人想拿?只能通过它提供的那个静态方法。嗯,这里要注意:单例模式不光管创建,还管生命周期。

单例模式的定义

定义其实很简单:保证一个类仅有一个实例,并提供一个全局访问点。这句话里藏着两个关键动作:

  • 控制实例数量:构造函数私有化,外部不能 new
  • 提供统一入口:通过静态方法返回唯一实例

我在项目中遇到过有人把构造函数写成 public,然后跟我说「我保证只 new 一次」——这种靠自觉的方式,迟早会出问题。单例模式就是用代码强制约束,而不是靠约定。

适用场景

不是所有类都需要做成单例。我总结了几类典型场景:

场景 说明 我踩过的坑
配置管理 全局配置只需加载一次 曾经把配置类做成多实例,结果不同模块读到不同配置值,排查了一整天
日志记录器 所有模块写同一个日志文件 多实例会导致日志交错、文件锁冲突
线程池 系统内线程资源统一管理 多个线程池实例容易把系统资源耗尽
数据库连接池 复用连接,避免频繁创建销毁 连接池单例没做好,生产环境直接 OOM
设备管理器 打印机、显卡等硬件只能有一个管理器 ——

你想想看,如果一个系统里有 10 个地方都在 new 配置对象,每个对象都要读一次配置文件——这不仅是性能问题,更可怕的是数据不一致。我曾经在一个支付系统里遇到过,因为配置对象不是单例,导致 A 模块读到的是旧配置,B 模块读到的是新配置,结果签名校验失败,线上事故。

饿汉式与懒汉式的核心区别

这两种实现方式,说白了就是「什么时候创建实例」的问题。

饿汉式

类加载的时候就创建实例。不管用不用,先 new 出来再说。

public class EagerSingleton {
    // 类加载时直接创建
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() {}
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}

优点很明显:线程安全,JVM 在类加载阶段就保证了只有一个线程能执行静态初始化。缺点呢?如果这个类很重,而且程序启动后一直没用它,那就白白浪费了资源。

我记得有一次做嵌入式开发,内存只有 64MB。用了饿汉式,结果启动时加载了一堆单例对象,直接 OOM。后来改成懒汉式才解决问题。

懒汉式

等到第一次调用 getInstance() 时才创建实例。说白了就是「懒加载」。

public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {}
    
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

这个版本有个大问题:线程不安全。两个线程同时进入 if 判断,就会创建两个实例。我曾经在生产环境遇到过,日志里出现了两个不同的实例地址,排查了半天才发现是懒汉式没加锁。

改进版——加个 synchronized:

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

但这样性能太差,每次调用都要加锁。更好的方式是双重检查锁(DCL):

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

这里 volatile 关键字很关键。我曾经见过有人漏掉 volatile,结果在并发场景下拿到的是半初始化的对象——那个 bug 查了我整整两天。

核心区别总结

对比维度 饿汉式 懒汉式
创建时机 类加载时 首次调用时
线程安全 天然安全 需要额外处理
资源占用 启动即占用 按需占用
实现复杂度 简单 较复杂(需考虑锁和 volatile)
适用场景 实例轻量、一定会被使用 实例重量、可能不被使用

我的建议:如果没有特殊理由,优先用饿汉式。简单、安全、不容易出错。懒汉式虽然灵活,但实现细节多,稍不注意就埋坑。

小技巧:如果既想要懒加载又想要线程安全,可以考虑用静态内部类方式。它利用了 JVM 的类加载机制,既延迟加载又天然线程安全。

警告:单例模式在分布式环境下会失效!每个 JVM 进程都有自己的实例。如果要做全局唯一,需要引入分布式锁或注册表方案。

知识体系图

单例模式 定义:唯一实例 + 全局访问点 适用场景:配置、日志、连接池等 实现方式 饿汉式 类加载时创建 懒汉式 首次调用时创建 ✓ 线程安全 ✓ 简单 ⚠ 需处理线程安全

这张图把单例模式的核心脉络理清楚了。从定义出发,到适用场景,再到两种实现方式的分支。你想想看,饿汉式和懒汉式就像两种性格——一个急性子,一个慢性子。选哪个,取决于你的业务场景和资源约束。

我个人更倾向于:能用饿汉式就别折腾懒汉式。除非你明确知道这个单例对象很重,而且大部分情况下不会被用到。我曾经在一个微服务网关里,把路由表做成了懒汉式单例,因为不是每个请求都需要加载全部路由规则。这种场景下,懒加载确实能省不少内存。

嗯,单例模式看似简单,但坑不少。多线程下的指令重排序、反射破坏单例、序列化破坏单例……这些进阶话题我们后面再聊。今天先把基础打牢:定义、场景、两种实现的核心区别,这三个点吃透了,单例模式你就掌握了八成。


公众号:蓝海资料掘金营,微信deep3321