单例模式:饿汉式、懒汉式、双重检查锁、静态内部类、枚举实现、C++实现对比
单例模式,说白了就是保证一个类只有一个实例。你可能会想,这有什么难的?new一个对象不就行了?嗯,但问题在于——你怎么确保整个系统里只new了一次?
我在项目中遇到过好几次这样的场景:配置文件加载器、日志记录器、数据库连接池……这些对象如果被多次实例化,轻则浪费内存,重则数据错乱、连接池耗尽。所以单例模式,是每个工程师必须掌握的看家本领。
核心要点:单例模式要解决两个问题——控制实例数量和提供全局访问点。说白了就是:你只能找我,而且我只有一个。
一、饿汉式
饿汉式,顾名思义,类加载的时候就迫不及待地把实例创建好。我刚开始学设计模式时,觉得这种方式最直接——反正早晚要用,先new出来再说。
// Java 饿汉式
public class SingletonEager {
private static final SingletonEager instance = new SingletonEager();
private SingletonEager() {}
public static SingletonEager getInstance() {
return instance;
}
}
优点很明显:线程安全,JVM在类加载时完成初始化,天然避免了多线程问题。缺点呢?如果这个类一直没被使用,实例就白白占着内存。我见过一个项目,启动时加载了十几个饿汉式单例,结果大部分都没用上……
我的建议:如果单例对象不大,且确定会被用到,饿汉式是最省心的选择。别为了那点内存纠结,代码简单才是王道。
二、懒汉式
懒汉式就聪明多了——用的时候才创建。但这里有个坑,你想想看:多线程环境下,两个线程同时调用getInstance(),会不会创建出两个实例?
// Java 懒汉式(非线程安全)
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy(); // 多线程下可能重复创建
}
return instance;
}
}
我曾经在线上环境踩过这个坑。一个配置管理器用了懒汉式,高并发下偶尔出现配置不一致的问题。排查了半天,最后发现是单例被创建了两次……
最简单的修复方式就是加synchronized:
// Java 懒汉式(线程安全,但性能差)
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
但这样每次调用都要加锁,性能损失很大。说白了,杀鸡用牛刀。
三、双重检查锁
双重检查锁(DCL)就是为了解决懒汉式的性能问题。先判断一次,不加锁;如果实例为空,再加锁创建。这样只有第一次创建时才需要同步。
// Java 双重检查锁
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可能会重排序,导致另一个线程拿到一个未完全初始化的对象。我曾经在面试中问过这个问题,能答对的人不到三成。
双重检查锁在JDK 5之后才完全可靠,因为volatile的语义在JDK 5中才被修复。如果你还在用老版本……嗯,赶紧升级吧。
四、静态内部类
这种方式我觉得是最优雅的Java实现。它利用了JVM的类加载机制——只有被主动引用时,内部类才会被加载。
// Java 静态内部类
public class SingletonHolder {
private SingletonHolder() {}
private static class Holder {
private static final SingletonHolder INSTANCE = new SingletonHolder();
}
public static SingletonHolder getInstance() {
return Holder.INSTANCE;
}
}
既实现了懒加载,又保证了线程安全,还没有锁的开销。我在实际项目中,90%的单例都用这种方式。你想想看,是不是很完美?
个人经验:静态内部类方式是我最推荐的Java单例实现。代码简洁,性能好,而且不容易出错。除非你需要序列化支持或者防止反射攻击,否则就用它。
五、枚举实现
Joshua Bloch在《Effective Java》中大力推荐枚举实现。为什么?因为它天然防止反射攻击和序列化破坏。
// Java 枚举实现
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
就这么简单。枚举的构造方法默认是私有的,JVM保证了枚举实例的唯一性。而且序列化时,枚举的序列化机制是特殊的,不会出现反序列化创建新实例的问题。
我曾经在一个金融项目中用过枚举单例,配合Spring Boot的配置管理,效果非常好。唯一的缺点?嗯,枚举不能继承,如果你需要继承某个类,那就不能用这种方式了。
六、C++实现对比
C++的实现思路和Java类似,但要注意内存管理。Java有GC,C++得自己操心。
// C++ 饿汉式
class SingletonEager {
public:
static SingletonEager& getInstance() {
return instance;
}
SingletonEager(const SingletonEager&) = delete;
SingletonEager& operator=(const SingletonEager&) = delete;
private:
SingletonEager() {}
static SingletonEager instance;
};
SingletonEager SingletonEager::instance;
// C++ 懒汉式(C++11线程安全)
class SingletonLazy {
public:
static SingletonLazy& getInstance() {
static SingletonLazy instance; // C++11保证局部静态变量初始化线程安全
return instance;
}
SingletonLazy(const SingletonLazy&) = delete;
SingletonLazy& operator=(const SingletonLazy&) = delete;
private:
SingletonLazy() {}
};
C++11之后,局部静态变量的初始化是线程安全的,所以懒汉式可以写得非常简单。但要注意,如果你用的是老编译器,还是得老老实实加锁。
避坑指南:我曾经在C++项目中用过双重检查锁,结果在某个老版本GCC上出了问题。后来我改用局部静态变量方式,再也没出过事。所以我的建议是——能用局部静态变量,就别折腾双重检查锁。
七、各实现方式对比
| 实现方式 | 懒加载 | 线程安全 | 性能 | 防反射/序列化 | 推荐场景 |
|---|---|---|---|---|---|
| 饿汉式 | 否 | 是 | 高 | 弱 | 对象小、必用 |
| 懒汉式(同步) | 是 | 是 | 低 | 弱 | 不推荐 |
| 双重检查锁 | 是 | 是 | 高 | 弱 | Java 5+ |
| 静态内部类 | 是 | 是 | 高 | 弱 | Java首选 |
| 枚举 | 否 | 是 | 高 | 强 | 需要防攻击 |
| C++局部静态 | 是 | 是 | 高 | N/A | C++11+首选 |
八、知识体系图
这张图把Java和C++的各种实现方式都梳理清楚了。我个人最推荐的是:Java用静态内部类,C++用局部静态变量。这两种方式代码最简洁,性能最好,也不容易出错。
最后说一句:设计模式不是死记硬背的,而是用来解决实际问题的。单例模式虽然简单,但用好了能避免很多坑。下次写代码时,想想你的场景适合哪种方式——别一上来就双重检查锁,也别什么都用饿汉式。选对了,事半功倍。