)
面试官问单例模式的7种写法与JVM层面分析一张图CEO招聘比喻彻底拿下这道必考题附图解比喻避坑指南预计阅读14分钟 你是不是也这样手写单例模式能写出双重检查锁DCL但面试官一追问“DCL为什么要加volatile”“反射能不能破坏单例”“枚举单例为什么最好”就答不上来了今天一张图 一个CEO招聘故事 7种写法深度剖析 JVM层面分析 六道追问彻底拿下这道题。摘要单例模式是Java中最基础也是最重要的创建型设计模式保证一个类在JVM中只有一个实例。本文完整梳理7种经典写法饿汉式线程安全、懒汉式非安全、synchronized懒汉式安全但低效、双重检查锁DCL、静态内部类完美懒加载、枚举单例最强防御和容器式单例Spring思想并从JVM层面深度分析类加载机制、指令重排与内存屏障DCL为何加volatile、序列化与反射攻击枚举如何防御。一句话饿汉简单不安全DCL加volatile防指令重排静态内部类最优雅枚举单例防御最强。我是折哥《Java 85题图解版》系列连载中已更新43题建议收藏本系列。每周2-3篇85题通关路线一键追完。点击关注第一时间收到每篇新题推送。上一篇面试官问Java模块化Project Jigsaw与反射限制全部85题点击查看总目录关注专栏追更不迷路一句话总结饿汉简单不安全DCL加volatile防指令重排静态内部类最优雅枚举单例防御最强。饿汉式线程安全但类加载即初始化 → 像公司还没开张CEO就已经定好了提前消耗资源。懒汉式非安全延迟加载但多线程不安全 → 像临时招CEO谁先抢到算谁的但两个人同时来可能招俩。synchronized懒汉式线程安全但每次获取锁性能极差 → 像前台锁门招CEO安全但每次都要锁门开门效率极低。DCL双重检查锁必须加volatile禁止指令重排 → 像两次确认再招人第一次快速看有没有没有就锁门再仔细确认没有才招。静态内部类最优雅类加载机制保证线程安全懒加载 → 像管家式招聘公司真正要人时才去招且JVM保证招聘过程绝对安全。枚举单例最强防御反射序列化都无法破坏 → 像CEO被写进《公司法》第一条任何人都不能修改、克隆、复制。背诵口诀饿汉安全但浪费懒汉不安全synchronized安全但慢DCL加volatile静态内部类最优雅枚举单例最强。核心设计理念单例 一个类只能有一个实例 提供一个全局访问点。 面试还原面试官手写一个线程安全的单例模式。你写的这种有什么问题还有更好的方式吗这是Java面试中手写代码出场率最高的设计模式题直接进入正题。 一图看懂单例模式7种写法进化史 生活比喻公司唯一的CEO场景设定一家公司JVM规定只能有一个CEO单例对象所有人线程都通过前台全局访问点联系CEO。① 饿汉式 面试当天就定下CEO公司还没开业类加载时董事会就定好了CEO创建实例。优点是CEO肯定有缺点是万一公司没开业未使用该类CEO就白招了内存浪费。② 懒汉式非安全 谁先抢到算谁的公司开张后需要CEO时才招人。如果两个人线程同时来前台要CEO前台可能同时招了两个CEO创建多个实例③ synchronized懒汉式 前台锁门前台说“每次只允许一个人进来招CEO”加锁。安全但每次都要锁门开门效率极低。④ DCL 两次检查锁前台先快速看一眼有没有CEO第一次检查没有就锁门加锁再仔细确认有没有第二次检查没有才招人。其他人来就直接拎走CEO不用锁门。JVM坑点编译器和CPU为了优化可能把“招人”和“贴CEO标签”的顺序颠倒。如果贴了标签但人还没完全招进来其他人拿到的就是个“半成品CEO”不完整的实例。所以必须加volatile禁止重排序。⑤ 静态内部类 管家式CEOCEO的招聘方案静态内部类提前设计好但只有公司真正要人时第一次调用管家类加载器才去执行招聘且JVM保证招聘过程绝对安全类加载机制天然线程安全。⑥ 枚举单例 写入宪法的CEOCEO不是“招聘”的而是写在《公司法》第一条枚举定义任何人都不能修改、不能克隆、不能反序列化复制。⑦ 容器式 CEO备选池公司准备了几个备用CEO候选人Map容器需要时根据key取用便于统一管理。 7种写法详解与JVM原理分析写法1饿汉式线程安全但不懒加载publicclassSingleton01{// 类加载时即创建实例——JVM保证线程安全privatestaticfinalSingleton01INSTANCEnewSingleton01();privateSingleton01(){}publicstaticSingleton01getInstance(){returnINSTANCE;}}优点实现简单、线程安全JVM类加载机制保证。缺点类加载即创建即使从未使用也占用内存。JVM原理static final变量在类加载的准备阶段分配内存初始化阶段执行构造由JVM保证线程安全。写法2懒汉式非线程安全publicclassSingleton02{privatestaticSingleton02instance;privateSingleton02(){}publicstaticSingleton02getInstance(){if(instancenull){instancenewSingleton02();// 线程不安全}returninstance;}}问题多线程下线程A和线程B同时进入if判断会创建两个实例。写法3synchronized懒汉式线程安全但性能差publicclassSingleton03{privatestaticSingleton03instance;privateSingleton03(){}publicstaticsynchronizedSingleton03getInstance(){if(instancenull){instancenewSingleton03();}returninstance;}}优点线程安全、懒加载。缺点方法级锁每次调用都加锁并发性能极差。写法4双重检查锁DCL—— 面试最高频publicclassSingleton04{// ⚠️ 必须加 volatile防止指令重排privatestaticvolatileSingleton04instance;privateSingleton04(){}publicstaticSingleton04getInstance(){if(instancenull){// 第一次检查synchronized(Singleton04.class){if(instancenull){// 第二次检查instancenewSingleton04();}}}returninstance;}}为什么加volatileinstance new Singleton04()在JVM层面分三步分配内存空间执行构造方法初始化对象将引用指向分配的内存地址JVM可能重排序为1→3→2。如果线程A执行完1→3此时instance ! null但对象还未初始化线程B进来直接返回instance拿到的是半成品对象volatile禁止指令重排保证2在3之前执行。写法5静态内部类最优雅、推荐publicclassSingleton05{privateSingleton05(){}// 静态内部类只有被调用时才会加载privatestaticclassHolder{privatestaticfinalSingleton05INSTANCEnewSingleton05();}publicstaticSingleton05getInstance(){returnHolder.INSTANCE;// 第一次调用时加载Holder类}}优点懒加载Holder类仅在调用getInstance时加载 线程安全类加载机制保证 无锁高性能。JVM原理Holder类只有在第一次访问时才会被类加载器加载加载的clinit方法由JVM内部加锁天然线程安全。写法6枚举单例最强防御publicenumSingleton06{INSTANCE;publicvoiddoSomething(){// 业务方法}}为什么最强天然线程安全枚举类加载机制保证反射攻击无效枚举的构造方法在反射中会被禁止调用序列化安全枚举的序列化机制保证反序列化返回同一个实例写法7容器式单例Spring思想publicclassSingleton07{privatestaticfinalMapString,ObjectcontainernewHashMap();publicstaticvoidregister(Stringkey,Objectinstance){if(!container.containsKey(key)){container.put(key,instance);}}publicstaticObjectget(Stringkey){returncontainer.get(key);}}Spring的IoC容器就是这种思想的延伸——统一管理单例Bean。️ JVM层面的防御机制深度解析1. 如何防止反射破坏单例写法能否被反射破坏原因普通类饿汉/懒汉/DCL/静态内部类✅能通过setAccessible(true)可调用私有构造器枚举单例❌不能枚举构造器在反射中默认被禁止2. 如何防止序列化破坏单例写法能否被序列化破坏防御方式普通类未处理✅能反序列化会创建新对象普通类已处理❌不能实现readResolve()返回INSTANCE枚举单例❌不能枚举序列化机制天然保证单例 高频面试追问6道大厂真题追问1DCL为什么要加volatile不加会怎样回答要点防止指令重排导致返回未初始化的半成品对象。详细回答instance new Singleton04()在JVM中并非原子操作分为3步①分配内存②构造初始化③引用指向内存。JVM可能重排序为①→③→②。如果线程A执行到③引用已指向内存但对象未初始化线程B调用getInstance()发现instance ! null直接返回拿到的是未初始化的对象使用时会崩溃。volatile禁止重排序保证②在③之前执行。追问2静态内部类和DCL哪个更好为什么回答要点静态内部类更简洁无锁无volatile推荐优先使用。详细回答静态内部类更优雅代码简洁无需synchronized和volatile利用JVM类加载机制天然保证线程安全懒加载。DCL适合需要额外控制如异常处理的场景但日常开发优先推荐静态内部类。追问3反射能破坏枚举单例吗为什么回答要点不能。反射API明确禁止通过反射创建枚举实例。详细回答在Constructor.newInstance()源码中如果类被标记为Enum会直接抛出IllegalArgumentExceptionif((clazz.getModifiers()Modifier.ENUM)!0)thrownewIllegalArgumentException(Cannot reflectively create enum objects);所以枚举单例是防御反射攻击的最强方案。追问4序列化会破坏枚举单例吗回答要点不会。枚举的序列化机制特殊反序列化返回同一个INSTANCE。详细回答普通Java对象反序列化会通过反射创建新对象。枚举不同——ObjectInputStream在反序列化枚举时会调用Enum.valueOf()返回已存在的枚举常量而不是创建新对象。追问5Spring的单例Bean和GOF单例模式有什么区别回答要点Spring单例Bean是“每个容器一个”GOF单例是“每个ClassLoader一个”。详细回答GOF单例保证全局唯一每个ClassLoader一个实例。Spring单例Bean保证每个IoC容器只有一个Bean实例不同容器可有不同实例。Spring单例更灵活支持依赖注入和AOP。追问6ClassLoader不同会破坏单例吗回答要点会。不同的ClassLoader会加载同一个类生成不同的Class对象从而创建不同实例。详细回答单例模式通过静态变量实现而静态变量属于Class。如果同一个类被两个不同的ClassLoader加载JVM中会存在两个不同的Class对象各自有静态变量因此会创建两个实例。这是单例模式的最大盲区。 避坑指南序号错误做法正确做法后果1DCL不加volatile必须加volatile拿到未初始化的半成品对象2单例类暴露公有无参构造器构造器私有化可通过new创建多个实例3忽略反射攻击普通单例使用枚举单例反射可创建新实例4单例类实现Serializable但不处理实现readResolve或用枚举反序列化可破坏单例5简单使用懒汉式非安全使用DCL或静态内部类多线程下产生多个实例 可运行验证代码importjava.lang.reflect.Constructor;importjava.io.*;publicclassSingletonTest{publicstaticvoidmain(String[]args)throwsException{// 1. DCL验证Singleton04dcl1Singleton04.getInstance();Singleton04dcl2Singleton04.getInstance();System.out.println(DCL: (dcl1dcl2));// true// 2. 静态内部类验证Singleton05inner1Singleton05.getInstance();Singleton05inner2Singleton05.getInstance();System.out.println(静态内部类: (inner1inner2));// true// 3. 反射攻击普通单例ConstructorSingleton05cSingleton05.class.getDeclaredConstructor();c.setAccessible(true);Singleton05reflectInstancec.newInstance();System.out.println(反射创建: (Singleton05.getInstance()reflectInstance));// false// 4. 反射攻击枚举单例会抛异常try{ConstructorSingleton06ecSingleton06.class.getDeclaredConstructor();ec.setAccessible(true);Singleton06enumReflectec.newInstance();}catch(IllegalArgumentExceptione){System.out.println(反射枚举: 被禁止! e.getMessage());}}}❓ 评论区挑战问题以下关于单例模式的描述哪一个是错误的publicclassSingleton{privatestaticvolatileSingletoninstance;publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}A.volatile用于禁止指令重排防止返回未初始化的对象B. synchronized保证同一时刻只有一个线程执行同步块C. 枚举单例无法被反射攻击D. 不同ClassLoader加载同一个单例类仍保证只有一个实例 欢迎在评论区写出你的答案和理由我会在下一篇文章发布后更新本文公布答案及错误选项逐项解析。✅ 答案公布正确答案D. 不同ClassLoader加载同一个单例类仍保证只有一个实例解析单例的静态变量属于Class级别不同的ClassLoader会加载出不同的Class对象各自拥有独立的静态变量因此不同ClassLoader下会创建多个实例单例被破坏选项A正确volatile禁止指令重排选项B正确synchronized保证互斥选项C正确枚举单例可防御反射攻击 总结写法懒加载线程安全反射防御序列化防御推荐度饿汉式❌✅❌❌⭐⭐懒汉式非安全✅❌❌❌❌synchronized懒汉式✅✅❌❌⭐⭐DCL✅✅❌❌⭐⭐⭐⭐静态内部类✅✅❌❌⭐⭐⭐⭐⭐枚举单例✅✅✅✅⭐⭐⭐⭐⭐面试官最看重的三个点DCL中volatile的作用禁止指令重排防止半成品对象静态内部类的原理类加载机制保证线程安全懒加载枚举单例为什么最强反射和序列化都无法破坏 系列导航上一篇面试官问Java模块化Project Jigsaw与反射限制全部85题目录点击查看关注专栏每周2-3篇一键追更搭配学习效果更佳本篇图解帮你快速建立知识画面记忆如果想深入理解源码实现和实战避坑细节可以配合姊妹系列《Java 100天进阶之路》对应章节一起学从零基础到上岗就业108篇完整学习地图每篇标配生活类比 可运行代码 避坑表 面试高频题 练习题不背八股文真正讲透“为什么”。 《Java 100天进阶之路》完整目录导航学习建议图解系列负责“快速建立知识图谱”进阶系列负责“深入理解原理”两个系列搭配使用面试备考效率翻倍。你在实际项目中被反射或序列化破坏过单例吗或者遇到过DCL不加volatile导致的诡异Bug欢迎评论区分享你的故事