单例模式(Singleton):饿汉式、懒汉式、双重检查锁、静态内部类、枚举实现、单例模式在Spring中的应用

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

我刚开始做 Java 开发那会儿,觉得单例模式挺简单的。后来踩了几个坑才发现,这里面门道不少。今天咱们就把六种实现方式掰开揉碎了讲清楚。

饿汉式:简单粗暴,但有点浪费

饿汉式,顾名思义,类加载的时候就把实例创建好了。不管用不用,先占个坑。

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

这种写法线程安全,因为类加载机制保证了初始化过程的同步。但问题也很明显——如果这个类一直没被用到,那实例就白白占着内存。

我的建议:如果单例对象不大,而且肯定会被用到,用饿汉式没问题。我在项目中写工具类时经常这么干。

懒汉式:用的时候再创建

懒汉式解决了饿汉式的浪费问题——等到第一次调用 getInstance() 时才创建实例。

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

注意这里加了 synchronized。不加会怎样?两个线程同时判断 instance == null,然后各自 new 一个,单例就破了。

我曾经踩过的坑:早期写代码时觉得 synchronized 影响性能,就把它去掉了。结果线上出现了两个不同的实例,排查了半天才发现是并发问题。嗯,从那以后我再也不敢在单例上偷懒了。

不过话说回来,synchronized 加在方法上,每次调用都要获取锁。如果 getInstance() 被频繁调用,性能确实会受影响。

双重检查锁:兼顾性能和线程安全

双重检查锁(DCL)的思路很巧妙——先判断一次 instance 是否为 null,不为 null 就直接返回,避免加锁。只有第一次创建时才进入同步块。

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

这里有个关键点:instance 必须用 volatile 修饰。为什么?因为 instance = new DCLSingleton() 不是原子操作。它分三步走:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

如果没有 volatile,JVM 可能重排序成 1→3→2。这时候另一个线程进来,发现 instance 不为 null,直接拿去用——结果对象还没初始化完,一用就崩。

核心要点:双重检查锁 + volatile 缺一不可。少了 volatile,双重检查锁就是纸老虎。

静态内部类:最优雅的懒加载

我个人最喜欢这种方式。它利用了类加载机制,既实现了懒加载,又保证了线程安全,还没有同步开销。

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

原理很简单:外部类加载时,内部类 Holder 不会立即加载。只有调用 getInstance() 时,Holder 才会被加载并初始化 INSTANCE。而类加载过程是线程安全的。

我在项目中用过很多次这种方式。代码简洁,性能也好,没什么副作用。

枚举实现:最安全的单例

枚举实现是《Effective Java》作者强烈推荐的。它能防止反射攻击和序列化破坏。

public enum EnumSingleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务逻辑
    }
}

为什么说它安全?因为 Java 规范保证了枚举实例的创建是线程安全的,而且反射 API 无法创建枚举实例。序列化方面,枚举的序列化机制也保证了不会产生新实例。

我的经验:如果你要写一个绝对安全的单例,用枚举就对了。不过在实际项目中,很多人觉得枚举写法不太"传统",所以用得不多。我个人觉得,安全第一,写法习惯可以改。

单例模式在 Spring 中的应用

Spring 容器中的 Bean 默认就是单例的。你想想看,Spring 管理的对象成千上万,如果每个 Bean 都创建多个实例,内存早爆了。

Spring 的单例实现和咱们上面讲的有点不一样。它用的是"注册表式单例"——把创建好的实例放到一个 Map 里,下次要的时候直接从 Map 取。

// Spring 源码中的核心逻辑(简化版)
public class DefaultSingletonBeanRegistry {
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    public Object getSingleton(String beanName) {
        return singletonObjects.get(beanName);
    }
    
    public void registerSingleton(String beanName, Object singletonObject) {
        singletonObjects.put(beanName, singletonObject);
    }
}

这种方式的优点是灵活——同一个类可以创建多个不同名称的实例,也可以控制哪些 Bean 是单例、哪些是原型。

避坑指南:Spring 的单例 Bean 默认是线程不安全的。如果你在 Bean 里放了可变的成员变量,多个请求同时访问就会出问题。我曾经在一个 Controller 里用了实例变量来存请求数据,结果用户 A 的数据被用户 B 覆盖了。解决方案很简单——要么用局部变量,要么把 Bean 的作用域改成 prototype。

六种实现方式对比

实现方式 线程安全 懒加载 防反射 防序列化 性能
饿汉式
懒汉式(同步方法)
双重检查锁
静态内部类
枚举
Spring 注册表式

知识体系总览

下面这张图把单例模式的六种实现和核心关注点串起来了。你可以看到,从最简单的饿汉式到最安全的枚举,每一步都是在解决某个特定问题。

单例模式六种实现 单例模式 饿汉式 懒汉式(同步方法) 双重检查锁 静态内部类 枚举实现 Spring注册表式 核心关注点:线程安全 · 懒加载 · 防反射 · 防序列化 · 性能

单例模式看起来简单,但每个实现背后都有它的设计考量。我个人建议,日常开发中优先考虑静态内部类或枚举实现。如果是在 Spring 项目里,直接用 Spring 的 Bean 管理就好,不用自己再写一套。

记住一点:单例模式不是为了炫技,而是为了解决"一个类只需要一个实例"这个实际问题。选哪种实现,取决于你的具体场景——要不要懒加载?要不要防反射?性能要求高不高?想清楚了再选。

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