单例模式:饿汉式、懒汉式、双重检查锁、静态内部类、枚举实现、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++的各种实现方式都梳理清楚了。我个人最推荐的是:Java用静态内部类,C++用局部静态变量。这两种方式代码最简洁,性能最好,也不容易出错。

最后说一句:设计模式不是死记硬背的,而是用来解决实际问题的。单例模式虽然简单,但用好了能避免很多坑。下次写代码时,想想你的场景适合哪种方式——别一上来就双重检查锁,也别什么都用饿汉式。选对了,事半功倍。

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