单例模式详解

单例模式是 23种设计模式中最简单、面试最高频、生产最常用 的创建型模式。几乎所有框架都大量使用单例:Spring Bean、线程池、配置工具类、缓存管理器、日志工具、连接池等。

很多人只会写饿汉、懒汉,但不懂:哪种写法线程安全?哪种有性能问题?哪种能防反射?哪种能防序列化破坏?生产到底用哪种?

本文一次性讲透单例模式核心思想、八种实现、优劣对比、底层隐患、安全加固、真实应用场景,看完彻底吃透单例所有考点。

一、什么是单例模式?核心定义

单例模式(Singleton) :保证一个类在整个JVM中仅有一个实例对象,并提供一个全局访问入口。

单例核心三大特征:

  1. 私有构造方法:禁止外部 new 对象

  2. 私有静态实例:类内部唯一实例

  3. 公有静态获取方法:对外统一获取实例入口

二、为什么需要单例?解决什么问题

2.1 核心优势

  • 节省内存:全局仅一个对象,避免频繁创建销毁对象,减少GC压力

  • 节省资源开销:连接池、线程池、配置类创建成本极高,必须全局唯一

  • 统一全局状态:全局配置、全局计数器、全局缓存必须保证唯一实例

  • 方便全局访问:无需传参,随处可获取实例

2.2 使用场景总结

只要是 工具类、配置类、资源池、全局管理器,都适合单例:

  • Spring 默认单例 Bean

  • 数据库连接池、线程池

  • 日志工具、脱敏工具、常量工具类

  • 全局配置信息、系统参数管理

  • 状态管理器、缓存管理器

三、单例八种写法(全网最全)+ 优劣+线程安全分析

重点:只有 3 种是生产可用:饿汉式、双重检查锁DCL、枚举单例,其余全部存在线程安全问题或性能问题。

1. 饿汉式(静态常量)------【线程安全、推荐简单场景】

原理:类加载阶段直接初始化实例,天然避免多线程竞争。

复制代码
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

优点:简单、线程绝对安全、无锁、性能高

缺点 :类加载即创建,存在内存浪费(不用也创建)

适用:小对象、必定使用的全局工具类

2. 饿汉式(静态代码块)

和上面完全一致,只是写法不同,同样类加载初始化。

3. 普通懒汉式 ---【线程不安全,禁止生产使用】

用到才创建,实现懒加载,但多线程下会创建多个实例。

问题:多线程同时判断 null,会穿透创建多个对象。

4. 同步方法懒汉式 ---【线程安全、性能极差】

方法加 synchronized,保证安全,但 每次获取实例都加锁,高并发严重卡顿。

结论:绝不生产使用。

5. 双重检查锁 DCL(经典生产写法)------【高并发最优】

业界标准写法、面试核心:兼顾懒加载、线程安全、高性能。

复制代码
public class Singleton {
    // volatile 禁止指令重排,解决半初始化问题
    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?

new 对象 底层三步:开辟内存 → 初始化对象 → 赋值引用

CPU 指令重排可能变成:开辟内存 → 赋值引用 → 初始化对象

导致其他线程获取到 半初始化脏对象,引发空指针、数据异常。

volatile 作用:禁止指令重排、保证可见性,彻底杜绝半初始化问题。

优点:懒加载、高并发无锁、性能极高

缺点:无法防止反射、序列化破坏

6. 静态内部类写法 ---【极简优雅、线程安全】

复制代码
public class Singleton {
    private Singleton(){}
    // 静态内部类:外部类加载不会加载内部类
    private static class Inner{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance(){
        return Inner.INSTANCE;
    }
}

原理:利用静态内部类延迟加载特性,天然线程安全。

优点:极简、懒加载、无锁、高性能

缺点:依然无法防反射、序列化破坏

7. 枚举单例 ---【最强安全、面试天花板】

复制代码
public enum SingletonEnum {
    INSTANCE;
}

《Effective Java》官方推荐最优单例

唯一优点

  • 天然线程安全

  • 天然防止反射破坏

  • 天然防止序列化破坏

  • 写法极简、无漏洞

唯一缺点:不支持懒加载(类加载即初始化)

生产结论能使用枚举优先枚举,安全等级最高。

四、单例两大致命漏洞:反射破坏 + 序列化破坏

这是面试 进阶必问:除枚举外,所有自定义单例都能被破解。

4.1 反射破坏单例

通过反射获取私有构造器、强行 new 多个对象,打破单例唯一性。

唯独枚举不能被反射破坏:JDK 底层禁止反射创建枚举实例,直接抛异常。

4.2 序列化破坏单例

对象序列化写入磁盘、再反序列化读取,会生成全新对象,破坏单例。

修复方案 :重写 readResolve() 方法,返回全局唯一实例。

枚举天然杜绝:序列化机制底层保证永远返回唯一枚举常量。

五、四种常用单例最终取舍(生产规范)

  1. 安全优先、无需懒加载 → 枚举单例(最优)

  2. 高并发、需要懒加载 → DCL 双重检查锁

  3. 简单工具类、无需懒加载 → 饿汉式

  4. 极简优雅、业务普通场景 → 静态内部类

六、Spring 单例与手写单例的区别

  • 手写单例:JVM 级别单例,全局永远唯一

  • Spring 单例容器级别单例,一个 IoC 容器一个实例,多容器依然多实例

Spring Bean 默认单例底层是通过 一级缓存 singletonObjects 实现单例复用。

七、单例模式优缺点总结

优点

  • 减少对象创建、节省内存、降低GC压力

  • 全局统一实例,状态统一、配置统一

  • 减少资源开销(连接池、线程池优势极大)

  • 全局访问便捷

缺点

  • 全局强耦合:过度使用导致代码依赖混乱

  • 无接口、不利于扩展(传统写法)

  • 并发漏洞多:写法不当极易线程不安全

  • 不利于单元测试:全局硬编码单例难以Mock

八、全文终极面试总结

1. 核心作用:保证类全局唯一实例,节省资源、统一状态、减少对象创建开销。

2. 线程安全写法:饿汉、静态内部类、DCL、枚举。

3. DCL volatile作用:禁止指令重排,防止获取半初始化脏对象。

4. 最强单例:枚举单例,天然防反射、防序列化、线程安全。

5. 漏洞根源:普通单例可被反射、序列化破坏;枚举JDK底层保护无解漏洞。

6. 生产选型:安全选枚举、并发懒加载选DCL、简单工具类选饿汉。

相关推荐
我叫张土豆1 小时前
本体论、DDD、SDD、TDD:AI 编程时代的四层认知栈
java·人工智能
白远山1 小时前
家政服务家政社区派单实战:调度系统架构设计与实现指南
java·开发语言·小程序·架构·需求分析
海南java第二人1 小时前
从诞生到回收:JVM对象生命周期与垃圾回收全链路解析
java
SL_staff2 小时前
JVS-Logic:从页面配置工具到企业级业务逻辑中枢的技术演进
java·算法·全栈
DevRay2 小时前
Java 21虚拟线程深度实战:告别线程池焦虑,百万并发吞吐量直接翻倍(原理+代码+避坑)
java·开发语言
forestsea2 小时前
从零构建 Java 智能体 RAG 系统:Milvus 向量数据库实战指南
java·数据库·milvus
lifewange2 小时前
VSCode怎么运行java
java·ide·vscode
用户094248568032 小时前
第10章:OpenJDK异常体系、栈轨迹与错误诊断入门
java·jvm
醉颜凉2 小时前
Java 必看:如何彻底避免 HashMap 多线程死循环问题?
java·开发语言