单例模式是一种创建型设计模式,其目的是:
保证一个类在系统中有且仅有一个实例,并提供一个全局访问点。
它适用于资源有限、全局状态共享、或需集中管理的场景。
举几个常见的使用场景:
•配置文件管理器(只加载一次)
•日志系统(全局统一记录)
•线程池、数据库连接池(节省资源)
•缓存、会话控制器(统一数据源)
•第三方 SDK(如微信、支付等 SDK 实例)
这些对象都具有以下特点:
1.全局唯一性:系统中只允许存在一个实例。
2.延迟加载需求:仅在第一次使用时创建。
3.线程安全要求:在并发环境中必须保证只创建一个实例。
假设你在一家公司工作,公司只有一台咖啡机☕️:
•所有人想喝咖啡,都要通过这台唯一的机器。
•不允许第二台,否则会导致资源浪费。
•同时多个人冲咖啡时,还要避免机器冲突(线程安全)。
这台咖啡机就是“单例”对象的现实版本。
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return instance;
}
}
✅优点:线程安全,简单明了
❌缺点:无法延迟加载,可能浪费资源
问题场景:
假设你在开发一个大型系统,定义了 ConfigManager 使用单例加载配置文件,结果由于饿汉式写法,它在系统启动时即加载文件,哪怕后续根本没使用这个模块,浪费了 500MB 的内存。
ConfigManager config = ConfigManager.getInstance(); // 实际代码中甚至可能从未调用
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
最终你以为日志是集中记录,其实是分开写的,分析出了大问题。
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 开销巨大。
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 重排序问题,极难复现,极难排查。
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,图像编码器只在真正处理图像时才需要。这个写法恰好满足“用时才加载”,又不牺牲线程安全,结构也优雅。
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("Using Enum Singleton");
}
}
✅ 优点:
•简洁
•天然线程安全
•防止反射破坏单例
•防止反序列化破坏单例
❌ 缺点:不支持懒加载
问题场景:
某个图形引擎用枚举单例加载纹理资源,但该资源仅在启动时用一次。结果类加载阶段就初始化,占用了 1GB 显存,导致移动设备直接 OOM。
实现方式 |
懒加载 |
线程安全 |
性能 |
是否推荐 |
---|---|---|---|---|
饿汉式 |
否 |
是 |
高 |
❌ |
懒汉式 |
是 |
否 |
高 |
❌ |
懒汉式 + 锁 |
是 |
是 |
低 |
⚠️ |
DCL |
是 |
是 |
高 |
✅ |
静态内部类 |
是 |
是 |
高 |
✅✅ |
枚举单例 |
否 |
是 |
高 |
✅ |
单例模式是设计模式中非常基础但非常实用的一种:
•概念简单,但在并发环境下实现不易。
•Java 推荐使用「静态内部类」或「枚举方式」。
•若需要延迟加载且线程安全,「DCL」是兼顾性能的好方案。
下一篇将带你深入讲解「工厂模式」的核心思想与进阶用法
如需Java面试题资料,可关注公众号:小健学Java,回复“面试”即可获得!
如果你觉得这篇对你有帮助,麻烦点赞收藏转发哦~