深入设计模式之「单例模式」:什么是单例?怎么写才优雅?

一、什么是单例模式(Singleton Pattern)?

单例模式是一种创建型设计模式,其目的是:

保证一个类在系统中有且仅有一个实例,并提供一个全局访问点。

它适用于资源有限、全局状态共享、或需集中管理的场景。


二、为什么我们需要单例?

举几个常见的使用场景:

•配置文件管理器(只加载一次)

•日志系统(全局统一记录)

•线程池、数据库连接池(节省资源)

•缓存、会话控制器(统一数据源)

•第三方 SDK(如微信、支付等 SDK 实例)

这些对象都具有以下特点:

1.全局唯一性:系统中只允许存在一个实例。

2.延迟加载需求:仅在第一次使用时创建。

3.线程安全要求:在并发环境中必须保证只创建一个实例。


三、用生活中的例子理解单例

假设你在一家公司工作,公司只有一台咖啡机☕️:

•所有人想喝咖啡,都要通过这台唯一的机器。

•不允许第二台,否则会导致资源浪费。

•同时多个人冲咖啡时,还要避免机器冲突(线程安全)。

这台咖啡机就是“单例”对象的现实版本。


四、Java 中的几种单例实现方式

1. 饿汉式(Eager Initialization)

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return instance;
    }
}

✅优点:线程安全,简单明了

❌缺点:无法延迟加载,可能浪费资源

问题场景:

假设你在开发一个大型系统,定义了 ConfigManager 使用单例加载配置文件,结果由于饿汉式写法,它在系统启动时即加载文件,哪怕后续根本没使用这个模块,浪费了 500MB 的内存。

ConfigManager config = ConfigManager.getInstance(); // 实际代码中甚至可能从未调用

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

public class LazyUnsafeSingleton {
    private static LazyUnsafeSingleton instance;

    private LazyUnsafeSingleton() {}

    public static LazyUnsafeSingleton getInstance() {
        if (instance == null) {
            instance = new LazyUnsafeSingleton(); // 并发问题
        }
        return instance;
    }
}

❌缺点:线程不安全

问题场景:

你在 Web 应用中,多个用户请求日志组件 Logger.getInstance() 来写日志。由于是高并发环境,两个线程同时判断 instance == null,结果各自都 new 出一个实例,日志写入出现错乱。

// Thread A:
Logger logA = Logger.getInstance(); // 创建实例A

// Thread B (同时运行):
Logger logB = Logger.getInstance(); // 创建实例B

最终你以为日志是集中记录,其实是分开写的,分析出了大问题。


3. 懒汉式(加同步,线程安全但性能差)

public class LazySafeSingleton {
    private static LazySafeSingleton instance;

    private LazySafeSingleton() {}

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

✅ 线程安全

❌ 缺点:每次都要加锁,性能低下

 问题场景:

在一个高频使用的工具类中,比如缓存服务 CacheManager,单例访问非常频繁。结果你加了同步,导致并发性能急剧下降,吞吐量下降 30%。

for (int i = 0; i < 10000; i++) {
    CacheManager.getInstance().get("key");
}

即便已经创建好了对象,还是不断加锁判断,CPU 开销巨大。


4.双重检查锁 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 会引发指令重排 bug

 问题场景:

某个开发者省略了 volatile:

// 错误写法
private static DCLSingleton instance; // 没加 volatile

结果对象还没完全构造完,其他线程就拿到了引用,出现空指针:

DCLSingleton singleton = DCLSingleton.getInstance();
singleton.doSomething(); // NullPointerException

这是一个经典 JVM 重排序问题,极难复现,极难排查。


5. 静态内部类(强烈推荐)

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

✅ 优点:

•JVM 保证线程安全

•延迟加载

•性能优秀

 适用场景:

假如你开发一个图像处理 SDK,图像编码器只在真正处理图像时才需要。这个写法恰好满足“用时才加载”,又不牺牲线程安全,结构也优雅。


6. 枚举单例(最安全)

public enum EnumSingleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("Using Enum Singleton");
    }
}

✅ 优点:

•简洁

•天然线程安全

•防止反射破坏单例

•防止反序列化破坏单例

❌ 缺点:不支持懒加载

 问题场景:

某个图形引擎用枚举单例加载纹理资源,但该资源仅在启动时用一次。结果类加载阶段就初始化,占用了 1GB 显存,导致移动设备直接 OOM。


五、单例模式的陷阱与对策

实现方式

懒加载

线程安全

性能

是否推荐

饿汉式

懒汉式

懒汉式 + 锁

⚠️

DCL

静态内部类

✅✅

枚举单例


六、总结

单例模式是设计模式中非常基础但非常实用的一种:

•概念简单,但在并发环境下实现不易。

•Java 推荐使用「静态内部类」或「枚举方式」。

•若需要延迟加载且线程安全,「DCL」是兼顾性能的好方案。


下一篇将带你深入讲解「工厂模式」的核心思想与进阶用法 ‍

如需Java面试题资料,可关注公众号:小健学Java,回复“面试”即可获得!
 

如果你觉得这篇对你有帮助,麻烦点赞收藏转发哦~

你可能感兴趣的:(设计模式,设计模式,单例模式,java)