Appearance
单例模式怎么写更稳:饿汉式、双重检查锁、静态内部类、枚举
单例模式看起来很简单,但它其实是很多人第一次真正接触“线程安全实现细节”的地方。
因为你不是只要“全局只有一个对象”,而是要在并发环境下仍然只创建出一个对象。
先说结论
在 Java 里,更常见也更稳的几个选择是:
- 简单直接:饿汉式
- 延迟加载且实现稳妥:静态内部类
- 最简洁也最抗反射/反序列化:枚举单例
- 双重检查锁可以用,但实现要非常规范
如果不是面试题,工程里优先考虑:
- 静态内部类
- 枚举单例
一、单例到底解决什么问题
单例适合那些天然只希望有一个实例的对象,例如:
- 配置中心客户端
- 连接池管理器
- 某些全局资源调度器
- 无状态工具型服务的统一入口
它的目标通常有两个:
- 保证全局唯一
- 避免重复创建重对象
二、饿汉式:最简单,也最直接
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}优点
- 线程安全
- 写法简单
缺点
- 类加载时就创建对象
- 如果这个对象很重、又不一定会用到,就会提前占资源
如果对象本身不重,而且一定会被用到,这种方式其实非常实用。
三、懒汉式:延迟加载,但要小心线程安全
很多人最早写单例会写成这样:
java
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}这在单线程里没问题,但多线程下可能会创建出多个实例,所以不能直接用于并发环境。
四、双重检查锁:可以用,但别漏掉 volatile
java
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}为什么需要 volatile
因为对象创建不是一个原子步骤,底层可能经历:
- 分配内存
- 初始化对象
- 把引用赋值给变量
如果发生指令重排,其他线程可能拿到一个“引用已经不为空,但对象还没完全初始化”的实例。
volatile 的作用,就是约束这类重排风险。
这种写法的特点
- 支持延迟加载
- 线程安全
- 写法比饿汉式复杂
所以它能用,但不是第一优先推荐。
五、静态内部类:延迟加载和实现简洁的平衡点
java
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
}为什么这种方式很实用
- 外部类加载时,不会立刻初始化内部类
- 第一次调用
getInstance()时,才触发内部类加载 - 类加载过程天然保证线程安全
所以它同时兼顾了:
- 延迟加载
- 线程安全
- 实现简洁
这也是工程里很常见的一种写法。
六、枚举单例:最简洁,也最抗打
java
public enum Singleton {
INSTANCE;
}这在很多场景下是最稳的单例写法,因为它天然防住了两个容易被忽略的问题:
- 反序列化破坏单例
- 反射破坏单例
如果你的单例不需要复杂继承层次,枚举方式非常值得优先考虑。
七、工程里怎么选
1. 对象一定会被用到,而且不重
用饿汉式,简单直接。
2. 希望延迟加载,又想保持代码简洁
用静态内部类。
3. 希望实现最稳,顺便规避反序列化和反射问题
用枚举单例。
4. 已经有固定风格,或者面试场景强调并发细节
可以写双重检查锁,但别漏掉 volatile。
八、单例模式最容易被误用的地方
1. 把太多可变状态塞进单例
单例只保证“对象唯一”,不保证“内部状态天然线程安全”。
2. 把单例当成全局变量仓库
这样很容易让依赖关系变隐式,代码反而更难维护。
3. 看到单例就以为一定更省资源
如果对象本身很轻,或者生命周期受容器管理,单例未必是重点优化方向。
一句话总结
单例模式最关键的不是记住几种写法,而是搞清楚线程安全和初始化时机。
如果只求实用,Java 工程里优先考虑静态内部类或枚举单例,通常已经足够稳。