单例模式(Singleton):一个类只能有一个实例

单例模式,说白了就是——一个类在整个系统里只能有一个对象。你想想看,有些东西多了反而麻烦,比如配置管理器、线程池、日志对象。如果每个地方都 new 一个,那资源就乱套了。

我个人习惯把单例模式看作是「全局唯一入口」。它保证了两件事:一是实例唯一,二是全局可访问。嗯,听起来简单,但实现起来有不少坑。我踩过不少,今天跟你聊聊。

为什么需要单例?

我在项目中遇到过这样一个场景:一个日志系统,如果每个模块都自己 new 一个 Logger,那日志文件会被多个实例同时写,内容乱成一锅粥。单例模式就能解决这个问题——所有代码都通过同一个实例写日志,顺序可控,性能也高。

常见的应用场景还有:

  • 数据库连接池
  • 配置信息读取器
  • 线程池管理器
  • 缓存系统

四种实现方式,各有千秋

单例模式的实现方式有好几种,我按「从简单到复杂」的顺序给你捋一遍。每种都有它的适用场景,别盲目选。

1. 饿汉式(Eager Initialization)

最简单粗暴的方式。类加载时就创建实例,不管你用不用。

public class SingletonEager {
    private static final SingletonEager instance = new SingletonEager();
    
    private SingletonEager() {}
    
    public static SingletonEager getInstance() {
        return instance;
    }
}

优点:线程安全,实现简单。
缺点:如果这个类很重,但一直没被使用,就白白占着内存。

我的建议:如果你的单例对象不重,或者一定会被用到,用饿汉式最省心。我早期做小项目时经常用,没出过问题。

2. 懒汉式(Lazy Initialization)

等到第一次调用 getInstance() 时才创建实例。节省资源,但要注意线程安全。

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

这里加了 synchronized 关键字,保证多线程环境下不会创建多个实例。但问题也很明显——每次调用 getInstance() 都要加锁,性能损耗大。

注意:我曾经在一个高并发系统里用过懒汉式,结果压测时发现 getInstance() 成了性能瓶颈。后来改成了双重检查锁,才解决问题。

3. 双重检查锁(Double-Checked Locking)

这个方案既保证了懒加载,又减少了锁的开销。只有第一次创建实例时才加锁,后续调用直接返回。

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

这里的关键是 volatile 关键字。为什么需要它?因为 instance = new SingletonDCL() 不是原子操作,JVM 可能会重排序,导致其他线程拿到一个「半初始化」的对象。volatile 禁止了指令重排,保证安全。

核心要点:双重检查锁是懒汉式的升级版,兼顾了性能和线程安全。但代码稍复杂,容易写错。我建议你直接记住这个模板,别自己瞎改。

4. 静态内部类(Static Inner Class)

这是我个人最喜欢的方式。它利用了 JVM 的类加载机制,既实现了懒加载,又天然线程安全,还没有锁的开销。

public class SingletonHolder {
    private SingletonHolder() {}
    
    private static class Holder {
        private static final SingletonHolder INSTANCE = new SingletonHolder();
    }
    
    public static SingletonHolder getInstance() {
        return Holder.INSTANCE;
    }
}

原理很简单:外部类加载时,内部类 Holder 并不会被加载。只有调用 getInstance() 时,JVM 才会加载 Holder,并创建实例。而 JVM 保证类加载过程是线程安全的。

我的推荐:如果你问我用哪种,我首选静态内部类。代码简洁,性能好,没有坑。我在好几个生产项目里都用它,从来没出过问题。

四种方式对比

实现方式 懒加载 线程安全 性能 代码复杂度
饿汉式
懒汉式 是(加锁)
双重检查锁
静态内部类

避坑指南

我曾经在面试中问过很多人:「单例模式有什么坑?」大部分人答不上来。这里我列几个常见的:

  • 反射破坏单例:通过反射调用私有构造器,可以创建多个实例。解决方案是在构造器里加一个标志位,第二次调用时抛异常。
  • 序列化破坏单例:如果单例类实现了 Serializable,反序列化时会创建新对象。需要重写 readResolve() 方法。
  • 多类加载器环境:不同类加载器会加载出不同的单例实例。这个比较少见,但如果你在 Web 容器里部署多个应用,要注意。
重要提醒:单例模式虽然好用,但别滥用。它本质上是全局状态,会让代码耦合变高,测试变难。我见过有人把几十个类都做成单例,最后维护起来简直噩梦。能用依赖注入解决的,就别用单例。

知识体系图

下面这张图帮你理清单例模式的核心脉络:

单例模式 饿汉式 类加载时创建 懒汉式 首次调用时创建 双重检查锁 volatile + 同步 静态内部类 JVM 类加载机制 核心原则 私有构造器 + 静态方法 + 唯一实例 线程安全 + 懒加载(按需选择)

总结

单例模式看起来简单,但实现细节里藏着不少门道。我个人建议:

  • 如果项目不大,用饿汉式最省事
  • 如果追求性能和懒加载,用静态内部类
  • 双重检查锁也行,但记得加 volatile
  • 懒汉式加锁的方式,能不用就别用了

嗯,单例模式就聊到这儿。记住一句话:唯一,但不滥用


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