[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-02":26,"\u002Farticles\u002Fdesign-pattern-02\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-02\u002Fadjacent":72,"\u002Fcategories":76,"render-design-pattern-02":102},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113930718034792450","design-pattern-02","设计模式修炼之路 ② 单例模式——一个实例，六种写法","一个类在整个应用里只允许存在一个实例——听着是一句话需求，做起来是一路打怪：懒汉式、饿汉式、双重校验锁、静态内部类、枚举，六种写法沿着\"线程安全\"这条主线层层演进。","2113897628365230081",1,1707,5238,14,"2023-09-24T10:50:03","> 一个类在整个应用里只允许存在一个实例——听着是一句话需求，做起来是一路打怪：懒汉式、饿汉式、双重校验锁、静态内部类、枚举，六种写法沿着\"线程安全\"这条主线层层演进。\n\n上一篇把地图摊开了，这一篇正式上路。创建型家族的第一个：单例模式，GoF 23 式里最简单的一个，也是面试出场率最高的一个。\n\n它要解决的问题只有一句话：**一个类在整个应用里只允许存在一个实例**。\n\n听着不像个问题？想想这些场景：配置管理器造出两份，一边改了配置另一边毫不知情；连接池造出两份，连接数直接翻倍；日志对象造出两份，两个句柄抢同一个文件。有些对象，天然只该有一份。\n\n## 一、什么是单例模式\n\n定义：**保证一个类仅有一个实例，并提供一个访问它的全局访问点**。\n\n落到实现上是三件事：\n\n1. 构造器私有——别人 new 不出来；\n2. 自己持有唯一实例——静态字段；\n3. 对外只开一扇门——静态的 getInstance()。\n\n三件事里，第 1、3 件是照抄的，真正千变万化的是第 2 件：**这个唯一的实例，什么时候造、怎么造得安全**。六种写法的全部差异，都在这一个问题里。\n\n## 二、优缺点\n\n**优点**：\n\n1. 内存里只有一个实例。对频繁创建销毁、或创建成本高的对象（大配置、重连接），省内存也省 CPU；\n2. 避免对资源的重复占用。写文件、连数据库这类独占型操作，两份实例就是事故现场；\n3. 访问点严格受控。什么时候拿、怎么拿、拿的是不是同一个，全收在一个方法里，全局行为可预期。\n\n**缺点**：\n\n1. **没有接口，不能继承**（常见类式写法都如此）。想换实现只能改源码——上一篇速览的开闭原则在这里直接顶牛：想\"对修改关闭\"，它不给你关；\n2. **一身二职**。单例类既当\"实例管理员\"又当业务类，单一职责原则亮红灯；\n3. **本质是受控的全局变量**。用爽了就是全局状态满天飞，模块之间长出看不见的依赖，谁动了状态全靠猜；\n4. **单元测试不友好**。全局状态在测试之间互相污染，想换成替身（mock）也得先破开它。\n\n## 三、应用场景\n\n- 有状态的管理类：配置管理、日志、连接池、线程池、缓存、ID 生成器；\n- JDK 里的活例子：Runtime.getRuntime() 是饿汉式单例，每个 Java 应用天生配一个运行时；Spring 容器里 Bean 的默认作用域也是单例；\n- 反过来提醒：无状态的纯工具类，直接 static 方法就够，不必强行单例——\"只许一份\"对无状态对象没有意义。\n\n## 四、实现方式：六种写法\n\n先交代裁判。光靠嘴说\"线程不安全\"太空洞，我写了个 SingletonTest：起 20 个线程同时去拿实例，把各自拿到的对象丢进 Set 里数一数——**Set 的大小，就是实际造出来的实例个数**。想让竞态稳定复现，难点在把\"同时\"做实，关键就一个字段加一段自旋：\n\n```java\nprivate static volatile boolean go = false;\n```\n\n```java\nnew Thread(() -> {\n    while (!go) {\n    }\n    instances.add(supplier.get());\n    doneGate.countDown();\n}).start();\n```\n\n主线程 sleep 100 毫秒，等 20 个线程全部在 while (!go) 上原地自旋\"各就各位\"，再把 go 置为 true——一声发令枪，20 个线程才真正同时冲进 getInstance()。（为什么不用 CountDownLatch 放行？await() 的唤醒自带先后顺序，20 个线程会被排着队一个个醒，竞态反而复现不出来。别问我怎么知道的。）\n\nsingleton 包里的全家福：\n\n```\nsingleton\n├── Singleton1.java    \u002F\u002F 懒汉式（线程不安全）\n├── Singleton2.java    \u002F\u002F 懒汉式（方法加锁）\n├── Singleton3.java    \u002F\u002F 饿汉式\n├── Singleton4.java    \u002F\u002F 双重校验锁\n├── Singleton5.java    \u002F\u002F 静态内部类\n├── Singleton6.java    \u002F\u002F 枚举\n└── SingletonTest.java \u002F\u002F 20 线程的裁判\n```\n\n### 1. 懒汉式（线程不安全）\n\n\"懒\"指用到才造（懒加载）：第一次调 getInstance() 时 instance 还是 null，才去 new。\n\n```java\npublic class Singleton1 {\n\n    private static Singleton1 instance;\n\n    private Singleton1() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton1 getInstance() {\n        if (instance == null) {\n            instance = new Singleton1();\n        }\n        return instance;\n    }\n}\n```\n\n构造函数里的打印是故意的，好从运行结果里看清对象被造了几次。裁判上场：\n\n```\n========== 第一版 懒汉式（线程不安全）：20 个线程同时起跑 ==========\n调用构造函数创建对象\n调用构造函数创建对象\n调用构造函数创建对象\n调用构造函数创建对象\n调用构造函数创建对象\n拿到 5 个不同的实例\n```\n\n问题出在那个 if 上——**判断和赋值不是原子的**。线程 A 刚看到 null 准备 new，线程 B 也看到 null 也去 new，各造各的。同一份代码连跑四次，分别拿到过 7 个、3 个、11 个、5 个实例：竞态的结果是随机的。\n\n这恰恰是最危险的地方：**不是每次都炸**。开发环境自测时也许 20 个线程挤成 1 个实例，上了高峰期一压测就冒出好几个。每次都触发的 bug 早就被修了；偶尔才触发的 bug，活得最久。\n\n### 2. 懒汉式（方法加锁）\n\n最直白的修法：给整个方法加上 synchronized，20 个线程排队过独木桥。\n\n```java\npublic class Singleton2 {\n\n    private static Singleton2 instance;\n\n    private Singleton2() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static synchronized Singleton2 getInstance() {\n        if (instance == null) {\n            instance = new Singleton2();\n        }\n        return instance;\n    }\n}\n```\n\n安全是安全了，裁判也确实只数出 1 个实例。但代价一眼可见：**锁把所有调用都挡住了**。真正需要锁的只有第一次创建那一下，之后哪怕几百万次纯读的 getInstance() 也得排队抢锁——为了 1 次写，把 999999 次读全赔了进去。能用，但亏。\n\n### 3. 饿汉式\n\n换个思路，不躲了：类加载的时候直接造好。\"饿\"就是等不及，见到就造。\n\n```java\npublic class Singleton3 {\n\n    private static Singleton3 instance = new Singleton3();\n\n    private Singleton3() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton3 getInstance() {\n        return instance;\n    }\n}\n```\n\n线程安全是白送的——JVM 保证类的初始化是排他的，一个类只初始化一次，连竞态窗口都没有。裁判数出 1 个实例。\n\n代价两个字：**浪费**。不管用不用得上，加载即占用；构造函数里若有重活（读配置、连远程），还会拖慢启动。反过来，如果这个实例大概率会用、构造也便宜，饿汉式是最省心的选择。\n\n### 4. 双重校验锁（DCL）\n\n前三版的合体改良：要懒加载（学第一版），要锁（学第二版），但只锁创建那一下（补第二版的亏）。这是面试高频题，两个问题必须答得上来。\n\n```java\npublic class Singleton4 {\n\n    private volatile static Singleton4 instance;\n\n    private Singleton4() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton4 getInstance() {\n        \u002F\u002F标记1\n        if (instance == null) {\n            \u002F\u002F标记2\n            synchronized (Singleton4.class) {\n                \u002F\u002F标记3\n                if (instance == null) {\n                    instance = new Singleton4();\n                }\n            }\n        }\n        \u002F\u002F标记4\n        return instance;\n    }\n}\n```\n\n**第一问：为什么要判两次 null？**\n\n- 标记 1 的外层判断是性能闸门：实例造好之后，绝大多数调用在这里就返回了，根本碰不到锁；\n- 标记 3 的内层判断是安全闸门：两个线程可以同时通过外层判断（都看到了 null），先到的抢到锁、造好实例、释放锁，后到的这时才进锁——如果不再检查一次 null，它会再造一个。\n\n一句话：**外层挡重复读，内层挡重复造**，一层都不能少。\n\n**第二问：为什么要 volatile？**\n\ninstance = new Singleton4() 不是一步到位，大致三步：分配内存、执行构造初始化对象、把引用赋给 instance。编译器和 CPU 可能把后两步重排成\"先赋引用、再初始化\"。重排之后，另一个线程在外层判断处看到 instance 已经非 null，兴冲冲拿走——拿到的是**还没初始化完的半成品**。volatile 禁掉这个重排，保证别人看到引用时，对象一定造完了。\n\n### 5. 静态内部类\n\nDCL 又难写又难讲。有没有安全 + 懒加载兼得、还不碰锁的写法？有——把实例挪进静态内部类：\n\n```java\npublic class Singleton5 {\n\n    public static class SingletonInner {\n        public static final Singleton5 INSTANCE = new Singleton5();\n    }\n\n    private Singleton5() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton5 getInstance() {\n        return SingletonInner.INSTANCE;\n    }\n}\n```\n\n两步看懂：\n\n- 外部类加载时，SingletonInner 不会跟着初始化（JVM 的类加载按需触发）——**懒加载达成**；\n- 第一次调 getInstance() 才触发内部类加载，而类初始化的排他性由 JVM 保证——**线程安全白送**，靠山和饿汉式是同一个。\n\n等于借 JVM 的类加载机制，把 DCL 手写的整套逻辑免费领了一遍，代码还短一半。要懒加载又怕写错 DCL，选它。\n\n### 6. 枚举\n\n《Effective Java》推荐的写法，一共三行：\n\n```java\npublic enum Singleton6 {\n    INSTANCE\n}\n```\n\n写法最短，保险却最厚，一口气三重：\n\n1. **线程安全**：枚举实例在类初始化时创建，JVM 保证；\n2. **防反射攻击**：JVM 明确禁止反射调用枚举构造器，newInstance() 直接抛异常；\n3. **防序列化破坏**：普通单例序列化再反序列化会造出新实例（得额外实现 readResolve() 才堵得住），枚举天然免疫，反序列化拿到的还是原来那个。\n\n反射和序列化这两个\"高级破坏手段\"，前五种写法全是裸奔，只有枚举全防住了。\n\n**六种写法之外，再看一个细节。**回头看六版 getInstance() 的返回类型：始终是具体的 Singleton1、Singleton2……调用方 import 的是具体类，依赖的也是具体类。哪天想换实现、或测试时想塞个替身进去，就得把每个调用点翻出来改——上一篇速览的六大原则里，**依赖倒置**（依赖抽象，不依赖具体实现）在这里轻轻响了声警报。单例自己解决不了这个问题，先把这句话记在小本本上，㉞ 收官篇回头细算。\n\n## 五、六种写法怎么选\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>写法\u003C\u002Fth>\u003Cth>懒加载\u003C\u002Fth>\u003Cth>线程安全\u003C\u002Fth>\u003Cth>高并发性能\u003C\u002Fth>\u003Cth>防反射\u002F序列化\u003C\u002Fth>\u003Cth>一句话点评\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>懒汉式（不安全）\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>反面教材，别进生产\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>懒汉式（方法加锁）\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>能用，但每次都排队\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>饿汉式\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>简单粗暴，构造不重就选它\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>双重校验锁\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>面试必考的工业级懒加载\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>静态内部类\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>常规首选，DCL 的平替\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>枚举\u003C\u002Ftd>\u003Ctd>✗\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>✓\u003C\u002Ftd>\u003Ctd>Effective Java 之选\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n裁判对 2～5 版的判决是一致的，构造函数都只跑一次：\n\n```\n========== 第二版 懒汉式（方法加锁）：20 个线程同时起跑 ==========\n调用构造函数创建对象\n拿到 1 个不同的实例\n```\n\n第六版直接比较两个引用：\n\n```\n========== 第六版 枚举 ==========\n两次获取是否同一个实例：true\n```\n\n实战建议很朴素：**不介意饿、构造便宜——第三版；要懒加载——第五版；担心反射或序列化搞破坏——第六版**。第一版永远别进生产代码；第二版留给性能无所谓的老代码；第四版——把它当成一道必须会做的面试题。\n\n## 写在最后\n\n六种写法翻完，主线只有一条：**线程安全**。从第一版随机的 7、3、11、5 个实例，到 DCL 的两层判断加 volatile，再到静态内部类和枚举干脆把安全外包给 JVM——所有演进都在回答同一个问题：怎么让\"只有一个实例\"在并发下依然成立。\n\n示例代码在 design-pattern 项目的 singleton 包里：Singleton1 到 Singleton6 是六种写法，SingletonTest 是那个 20 线程的裁判，跑一次 main 就能看到竞态现场——顺便说，每次跑出来的数都不一样。\n\n单例管的是\"一个类有几个实例\"，但创建型家族真正的主场是另一件事：**实例从哪来**。一堆同族对象，怎么造才不至于让调用方把 new 写得到处都是、改一个产品名就全局翻车？下一篇——**工厂模式**。","\u003Cblockquote>\n \u003Cp>一个类在整个应用里只允许存在一个实例——听着是一句话需求，做起来是一路打怪：懒汉式、饿汉式、双重校验锁、静态内部类、枚举，六种写法沿着\"线程安全\"这条主线层层演进。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>上一篇把地图摊开了，这一篇正式上路。创建型家族的第一个：单例模式，GoF 23 式里最简单的一个，也是面试出场率最高的一个。\u003C\u002Fp>\n\u003Cp>它要解决的问题只有一句话：\u003Cstrong>一个类在整个应用里只允许存在一个实例\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>听着不像个问题？想想这些场景：配置管理器造出两份，一边改了配置另一边毫不知情；连接池造出两份，连接数直接翻倍；日志对象造出两份，两个句柄抢同一个文件。有些对象，天然只该有一份。\u003C\u002Fp>\n\u003Ch2>一、什么是单例模式\u003C\u002Fh2>\n\u003Cp>定义：\u003Cstrong>保证一个类仅有一个实例，并提供一个访问它的全局访问点\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>落到实现上是三件事：\u003C\u002Fp>\n\u003Col>\n \u003Cli>构造器私有——别人 new 不出来；\u003C\u002Fli>\n \u003Cli>自己持有唯一实例——静态字段；\u003C\u002Fli>\n \u003Cli>对外只开一扇门——静态的 getInstance()。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>三件事里，第 1、3 件是照抄的，真正千变万化的是第 2 件：\u003Cstrong>这个唯一的实例，什么时候造、怎么造得安全\u003C\u002Fstrong>。六种写法的全部差异，都在这一个问题里。\u003C\u002Fp>\n\u003Ch2>二、优缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Col>\n \u003Cli>内存里只有一个实例。对频繁创建销毁、或创建成本高的对象（大配置、重连接），省内存也省 CPU；\u003C\u002Fli>\n \u003Cli>避免对资源的重复占用。写文件、连数据库这类独占型操作，两份实例就是事故现场；\u003C\u002Fli>\n \u003Cli>访问点严格受控。什么时候拿、怎么拿、拿的是不是同一个，全收在一个方法里，全局行为可预期。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>没有接口，不能继承\u003C\u002Fstrong>（常见类式写法都如此）。想换实现只能改源码——上一篇速览的开闭原则在这里直接顶牛：想\"对修改关闭\"，它不给你关；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>一身二职\u003C\u002Fstrong>。单例类既当\"实例管理员\"又当业务类，单一职责原则亮红灯；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>本质是受控的全局变量\u003C\u002Fstrong>。用爽了就是全局状态满天飞，模块之间长出看不见的依赖，谁动了状态全靠猜；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>单元测试不友好\u003C\u002Fstrong>。全局状态在测试之间互相污染，想换成替身（mock）也得先破开它。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>有状态的管理类：配置管理、日志、连接池、线程池、缓存、ID 生成器；\u003C\u002Fli>\n \u003Cli>JDK 里的活例子：Runtime.getRuntime() 是饿汉式单例，每个 Java 应用天生配一个运行时；Spring 容器里 Bean 的默认作用域也是单例；\u003C\u002Fli>\n \u003Cli>反过来提醒：无状态的纯工具类，直接 static 方法就够，不必强行单例——\"只许一份\"对无状态对象没有意义。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>四、实现方式：六种写法\u003C\u002Fh2>\n\u003Cp>先交代裁判。光靠嘴说\"线程不安全\"太空洞，我写了个 SingletonTest：起 20 个线程同时去拿实例，把各自拿到的对象丢进 Set 里数一数——\u003Cstrong>Set 的大小，就是实际造出来的实例个数\u003C\u002Fstrong>。想让竞态稳定复现，难点在把\"同时\"做实，关键就一个字段加一段自旋：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">private static volatile boolean go = false;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode class=\"language-java\">new Thread(() -&gt; {\n    while (!go) {\n    }\n    instances.add(supplier.get());\n    doneGate.countDown();\n}).start();\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>主线程 sleep 100 毫秒，等 20 个线程全部在 while (!go) 上原地自旋\"各就各位\"，再把 go 置为 true——一声发令枪，20 个线程才真正同时冲进 getInstance()。（为什么不用 CountDownLatch 放行？await() 的唤醒自带先后顺序，20 个线程会被排着队一个个醒，竞态反而复现不出来。别问我怎么知道的。）\u003C\u002Fp>\n\u003Cp>singleton 包里的全家福：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>singleton\n├── Singleton1.java    \u002F\u002F 懒汉式（线程不安全）\n├── Singleton2.java    \u002F\u002F 懒汉式（方法加锁）\n├── Singleton3.java    \u002F\u002F 饿汉式\n├── Singleton4.java    \u002F\u002F 双重校验锁\n├── Singleton5.java    \u002F\u002F 静态内部类\n├── Singleton6.java    \u002F\u002F 枚举\n└── SingletonTest.java \u002F\u002F 20 线程的裁判\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>1. 懒汉式（线程不安全）\u003C\u002Fh3>\n\u003Cp>\"懒\"指用到才造（懒加载）：第一次调 getInstance() 时 instance 还是 null，才去 new。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Singleton1 {\n\n    private static Singleton1 instance;\n\n    private Singleton1() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton1 getInstance() {\n        if (instance == null) {\n            instance = new Singleton1();\n        }\n        return instance;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>构造函数里的打印是故意的，好从运行结果里看清对象被造了几次。裁判上场：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 第一版 懒汉式（线程不安全）：20 个线程同时起跑 ==========\n调用构造函数创建对象\n调用构造函数创建对象\n调用构造函数创建对象\n调用构造函数创建对象\n调用构造函数创建对象\n拿到 5 个不同的实例\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>问题出在那个 if 上——\u003Cstrong>判断和赋值不是原子的\u003C\u002Fstrong>。线程 A 刚看到 null 准备 new，线程 B 也看到 null 也去 new，各造各的。同一份代码连跑四次，分别拿到过 7 个、3 个、11 个、5 个实例：竞态的结果是随机的。\u003C\u002Fp>\n\u003Cp>这恰恰是最危险的地方：\u003Cstrong>不是每次都炸\u003C\u002Fstrong>。开发环境自测时也许 20 个线程挤成 1 个实例，上了高峰期一压测就冒出好几个。每次都触发的 bug 早就被修了；偶尔才触发的 bug，活得最久。\u003C\u002Fp>\n\u003Ch3>2. 懒汉式（方法加锁）\u003C\u002Fh3>\n\u003Cp>最直白的修法：给整个方法加上 synchronized，20 个线程排队过独木桥。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Singleton2 {\n\n    private static Singleton2 instance;\n\n    private Singleton2() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static synchronized Singleton2 getInstance() {\n        if (instance == null) {\n            instance = new Singleton2();\n        }\n        return instance;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>安全是安全了，裁判也确实只数出 1 个实例。但代价一眼可见：\u003Cstrong>锁把所有调用都挡住了\u003C\u002Fstrong>。真正需要锁的只有第一次创建那一下，之后哪怕几百万次纯读的 getInstance() 也得排队抢锁——为了 1 次写，把 999999 次读全赔了进去。能用，但亏。\u003C\u002Fp>\n\u003Ch3>3. 饿汉式\u003C\u002Fh3>\n\u003Cp>换个思路，不躲了：类加载的时候直接造好。\"饿\"就是等不及，见到就造。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Singleton3 {\n\n    private static Singleton3 instance = new Singleton3();\n\n    private Singleton3() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton3 getInstance() {\n        return instance;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>线程安全是白送的——JVM 保证类的初始化是排他的，一个类只初始化一次，连竞态窗口都没有。裁判数出 1 个实例。\u003C\u002Fp>\n\u003Cp>代价两个字：\u003Cstrong>浪费\u003C\u002Fstrong>。不管用不用得上，加载即占用；构造函数里若有重活（读配置、连远程），还会拖慢启动。反过来，如果这个实例大概率会用、构造也便宜，饿汉式是最省心的选择。\u003C\u002Fp>\n\u003Ch3>4. 双重校验锁（DCL）\u003C\u002Fh3>\n\u003Cp>前三版的合体改良：要懒加载（学第一版），要锁（学第二版），但只锁创建那一下（补第二版的亏）。这是面试高频题，两个问题必须答得上来。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Singleton4 {\n\n    private volatile static Singleton4 instance;\n\n    private Singleton4() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton4 getInstance() {\n        \u002F\u002F标记1\n        if (instance == null) {\n            \u002F\u002F标记2\n            synchronized (Singleton4.class) {\n                \u002F\u002F标记3\n                if (instance == null) {\n                    instance = new Singleton4();\n                }\n            }\n        }\n        \u002F\u002F标记4\n        return instance;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第一问：为什么要判两次 null？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>标记 1 的外层判断是性能闸门：实例造好之后，绝大多数调用在这里就返回了，根本碰不到锁；\u003C\u002Fli>\n \u003Cli>标记 3 的内层判断是安全闸门：两个线程可以同时通过外层判断（都看到了 null），先到的抢到锁、造好实例、释放锁，后到的这时才进锁——如果不再检查一次 null，它会再造一个。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>一句话：\u003Cstrong>外层挡重复读，内层挡重复造\u003C\u002Fstrong>，一层都不能少。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>第二问：为什么要 volatile？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>instance = new Singleton4() 不是一步到位，大致三步：分配内存、执行构造初始化对象、把引用赋给 instance。编译器和 CPU 可能把后两步重排成\"先赋引用、再初始化\"。重排之后，另一个线程在外层判断处看到 instance 已经非 null，兴冲冲拿走——拿到的是\u003Cstrong>还没初始化完的半成品\u003C\u002Fstrong>。volatile 禁掉这个重排，保证别人看到引用时，对象一定造完了。\u003C\u002Fp>\n\u003Ch3>5. 静态内部类\u003C\u002Fh3>\n\u003Cp>DCL 又难写又难讲。有没有安全 + 懒加载兼得、还不碰锁的写法？有——把实例挪进静态内部类：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class Singleton5 {\n\n    public static class SingletonInner {\n        public static final Singleton5 INSTANCE = new Singleton5();\n    }\n\n    private Singleton5() {\n        System.out.println(\"调用构造函数创建对象\");\n    }\n\n    public static Singleton5 getInstance() {\n        return SingletonInner.INSTANCE;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>两步看懂：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>外部类加载时，SingletonInner 不会跟着初始化（JVM 的类加载按需触发）——\u003Cstrong>懒加载达成\u003C\u002Fstrong>；\u003C\u002Fli>\n \u003Cli>第一次调 getInstance() 才触发内部类加载，而类初始化的排他性由 JVM 保证——\u003Cstrong>线程安全白送\u003C\u002Fstrong>，靠山和饿汉式是同一个。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>等于借 JVM 的类加载机制，把 DCL 手写的整套逻辑免费领了一遍，代码还短一半。要懒加载又怕写错 DCL，选它。\u003C\u002Fp>\n\u003Ch3>6. 枚举\u003C\u002Fh3>\n\u003Cp>《Effective Java》推荐的写法，一共三行：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public enum Singleton6 {\n    INSTANCE\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>写法最短，保险却最厚，一口气三重：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>线程安全\u003C\u002Fstrong>：枚举实例在类初始化时创建，JVM 保证；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>防反射攻击\u003C\u002Fstrong>：JVM 明确禁止反射调用枚举构造器，newInstance() 直接抛异常；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>防序列化破坏\u003C\u002Fstrong>：普通单例序列化再反序列化会造出新实例（得额外实现 readResolve() 才堵得住），枚举天然免疫，反序列化拿到的还是原来那个。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>反射和序列化这两个\"高级破坏手段\"，前五种写法全是裸奔，只有枚举全防住了。\u003C\u002Fp>\n\u003Cp>**六种写法之外，再看一个细节。**回头看六版 getInstance() 的返回类型：始终是具体的 Singleton1、Singleton2……调用方 import 的是具体类，依赖的也是具体类。哪天想换实现、或测试时想塞个替身进去，就得把每个调用点翻出来改——上一篇速览的六大原则里，\u003Cstrong>依赖倒置\u003C\u002Fstrong>（依赖抽象，不依赖具体实现）在这里轻轻响了声警报。单例自己解决不了这个问题，先把这句话记在小本本上，㉞ 收官篇回头细算。\u003C\u002Fp>\n\u003Ch2>五、六种写法怎么选\u003C\u002Fh2>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>写法\u003C\u002Fth>\n   \u003Cth>懒加载\u003C\u002Fth>\n   \u003Cth>线程安全\u003C\u002Fth>\n   \u003Cth>高并发性能\u003C\u002Fth>\n   \u003Cth>防反射\u002F序列化\u003C\u002Fth>\n   \u003Cth>一句话点评\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>懒汉式（不安全）\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>反面教材，别进生产\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>懒汉式（方法加锁）\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>能用，但每次都排队\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>饿汉式\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>简单粗暴，构造不重就选它\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>双重校验锁\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>面试必考的工业级懒加载\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>静态内部类\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>常规首选，DCL 的平替\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>枚举\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>Effective Java 之选\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>裁判对 2～5 版的判决是一致的，构造函数都只跑一次：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 第二版 懒汉式（方法加锁）：20 个线程同时起跑 ==========\n调用构造函数创建对象\n拿到 1 个不同的实例\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第六版直接比较两个引用：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 第六版 枚举 ==========\n两次获取是否同一个实例：true\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>实战建议很朴素：\u003Cstrong>不介意饿、构造便宜——第三版；要懒加载——第五版；担心反射或序列化搞破坏——第六版\u003C\u002Fstrong>。第一版永远别进生产代码；第二版留给性能无所谓的老代码；第四版——把它当成一道必须会做的面试题。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cp>六种写法翻完，主线只有一条：\u003Cstrong>线程安全\u003C\u002Fstrong>。从第一版随机的 7、3、11、5 个实例，到 DCL 的两层判断加 volatile，再到静态内部类和枚举干脆把安全外包给 JVM——所有演进都在回答同一个问题：怎么让\"只有一个实例\"在并发下依然成立。\u003C\u002Fp>\n\u003Cp>示例代码在 design-pattern 项目的 singleton 包里：Singleton1 到 Singleton6 是六种写法，SingletonTest 是那个 20 线程的裁判，跑一次 main 就能看到竞态现场——顺便说，每次跑出来的数都不一样。\u003C\u002Fp>\n\u003Cp>单例管的是\"一个类有几个实例\"，但创建型家族真正的主场是另一件事：\u003Cstrong>实例从哪来\u003C\u002Fstrong>。一堆同族对象，怎么造才不至于让调用方把 new 写得到处都是、改一个产品名就全局翻车？下一篇——\u003Cstrong>工厂模式\u003C\u002Fstrong>。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113931131085656066","design-pattern-03","设计模式修炼之路 ③ 工厂模式——简单工厂、工厂方法、抽象工厂三兄弟","简单工厂、工厂方法、抽象工厂三兄弟的演进与取舍、开闭原则得失",3740,6415,17,"2023-10-13T21:36:05",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113907939247919106","design-pattern-01","设计模式修炼之路 ① 初识设计模式——先把地图摊开","这是系列的第一篇，没有一行代码：设计模式是什么、模式分哪四大家族、六大原则长什么样。先把地图摊开，路从下一篇开始走。",3267,1903,5,"2023-09-10T12:19:35",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113931247385317377","design-pattern-05","设计模式修炼之路 ⑤ 原型模式——clone() 的浅与深","clone() 与 Cloneable、浅拷贝踩坑、逐字段克隆与序列化两种深拷贝",2661,5758,15,"2023-11-17T17:12:14",{"code":4,"message":5,"data":73},{"prev":74,"next":75},{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},{"code":4,"message":5,"data":77},[78,84,89,95],{"id":79,"name":80,"slug":81,"description":82,"sortOrder":4,"articleCount":83},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":85,"slug":86,"description":87,"sortOrder":4,"articleCount":88},"设计模式","design-pattern","设计模式系列","34",{"id":90,"name":91,"slug":92,"description":93,"sortOrder":33,"articleCount":94},"1946000000000000002","随笔","essay","生活与思考","0",{"id":96,"name":97,"slug":98,"description":99,"sortOrder":100,"articleCount":101},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":103,"toc":104},"\u003Cblockquote>\n \u003Cp>一个类在整个应用里只允许存在一个实例——听着是一句话需求，做起来是一路打怪：懒汉式、饿汉式、双重校验锁、静态内部类、枚举，六种写法沿着\"线程安全\"这条主线层层演进。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>上一篇把地图摊开了，这一篇正式上路。创建型家族的第一个：单例模式，GoF 23 式里最简单的一个，也是面试出场率最高的一个。\u003C\u002Fp>\n\u003Cp>它要解决的问题只有一句话：\u003Cstrong>一个类在整个应用里只允许存在一个实例\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>听着不像个问题？想想这些场景：配置管理器造出两份，一边改了配置另一边毫不知情；连接池造出两份，连接数直接翻倍；日志对象造出两份，两个句柄抢同一个文件。有些对象，天然只该有一份。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、什么是单例模式\u003C\u002Fh2>\n\u003Cp>定义：\u003Cstrong>保证一个类仅有一个实例，并提供一个访问它的全局访问点\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>落到实现上是三件事：\u003C\u002Fp>\n\u003Col>\n \u003Cli>构造器私有——别人 new 不出来；\u003C\u002Fli>\n \u003Cli>自己持有唯一实例——静态字段；\u003C\u002Fli>\n \u003Cli>对外只开一扇门——静态的 getInstance()。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>三件事里，第 1、3 件是照抄的，真正千变万化的是第 2 件：\u003Cstrong>这个唯一的实例，什么时候造、怎么造得安全\u003C\u002Fstrong>。六种写法的全部差异，都在这一个问题里。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Col>\n \u003Cli>内存里只有一个实例。对频繁创建销毁、或创建成本高的对象（大配置、重连接），省内存也省 CPU；\u003C\u002Fli>\n \u003Cli>避免对资源的重复占用。写文件、连数据库这类独占型操作，两份实例就是事故现场；\u003C\u002Fli>\n \u003Cli>访问点严格受控。什么时候拿、怎么拿、拿的是不是同一个，全收在一个方法里，全局行为可预期。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>没有接口，不能继承\u003C\u002Fstrong>（常见类式写法都如此）。想换实现只能改源码——上一篇速览的开闭原则在这里直接顶牛：想\"对修改关闭\"，它不给你关；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>一身二职\u003C\u002Fstrong>。单例类既当\"实例管理员\"又当业务类，单一职责原则亮红灯；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>本质是受控的全局变量\u003C\u002Fstrong>。用爽了就是全局状态满天飞，模块之间长出看不见的依赖，谁动了状态全靠猜；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>单元测试不友好\u003C\u002Fstrong>。全局状态在测试之间互相污染，想换成替身（mock）也得先破开它。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、应用场景\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>有状态的管理类：配置管理、日志、连接池、线程池、缓存、ID 生成器；\u003C\u002Fli>\n \u003Cli>JDK 里的活例子：Runtime.getRuntime() 是饿汉式单例，每个 Java 应用天生配一个运行时；Spring 容器里 Bean 的默认作用域也是单例；\u003C\u002Fli>\n \u003Cli>反过来提醒：无状态的纯工具类，直接 static 方法就够，不必强行单例——\"只许一份\"对无状态对象没有意义。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-4\">四、实现方式：六种写法\u003C\u002Fh2>\n\u003Cp>先交代裁判。光靠嘴说\"线程不安全\"太空洞，我写了个 SingletonTest：起 20 个线程同时去拿实例，把各自拿到的对象丢进 Set 里数一数——\u003Cstrong>Set 的大小，就是实际造出来的实例个数\u003C\u002Fstrong>。想让竞态稳定复现，难点在把\"同时\"做实，关键就一个字段加一段自旋：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> volatile\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> go \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Thread\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    while\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">go) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    instances.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(supplier.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    doneGate.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">countDown\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">start\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>主线程 sleep 100 毫秒，等 20 个线程全部在 while (!go) 上原地自旋\"各就各位\"，再把 go 置为 true——一声发令枪，20 个线程才真正同时冲进 getInstance()。（为什么不用 CountDownLatch 放行？await() 的唤醒自带先后顺序，20 个线程会被排着队一个个醒，竞态反而复现不出来。别问我怎么知道的。）\u003C\u002Fp>\n\u003Cp>singleton 包里的全家福：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>singleton\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── Singleton1.java    \u002F\u002F 懒汉式（线程不安全）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── Singleton2.java    \u002F\u002F 懒汉式（方法加锁）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── Singleton3.java    \u002F\u002F 饿汉式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── Singleton4.java    \u002F\u002F 双重校验锁\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── Singleton5.java    \u002F\u002F 静态内部类\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>├── Singleton6.java    \u002F\u002F 枚举\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>└── SingletonTest.java \u002F\u002F 20 线程的裁判\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"toc-5\">1. 懒汉式（线程不安全）\u003C\u002Fh3>\n\u003Cp>\"懒\"指用到才造（懒加载）：第一次调 getInstance() 时 instance 还是 null，才去 new。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton1 instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"调用构造函数创建对象\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton1 \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getInstance\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>构造函数里的打印是故意的，好从运行结果里看清对象被造了几次。裁判上场：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 第一版 懒汉式（线程不安全）：20 个线程同时起跑 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>调用构造函数创建对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>调用构造函数创建对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>调用构造函数创建对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>调用构造函数创建对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>调用构造函数创建对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>拿到 5 个不同的实例\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>问题出在那个 if 上——\u003Cstrong>判断和赋值不是原子的\u003C\u002Fstrong>。线程 A 刚看到 null 准备 new，线程 B 也看到 null 也去 new，各造各的。同一份代码连跑四次，分别拿到过 7 个、3 个、11 个、5 个实例：竞态的结果是随机的。\u003C\u002Fp>\n\u003Cp>这恰恰是最危险的地方：\u003Cstrong>不是每次都炸\u003C\u002Fstrong>。开发环境自测时也许 20 个线程挤成 1 个实例，上了高峰期一压测就冒出好几个。每次都触发的 bug 早就被修了；偶尔才触发的 bug，活得最久。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">2. 懒汉式（方法加锁）\u003C\u002Fh3>\n\u003Cp>最直白的修法：给整个方法加上 synchronized，20 个线程排队过独木桥。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton2 instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"调用构造函数创建对象\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> synchronized\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton2 \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getInstance\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton2\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>安全是安全了，裁判也确实只数出 1 个实例。但代价一眼可见：\u003Cstrong>锁把所有调用都挡住了\u003C\u002Fstrong>。真正需要锁的只有第一次创建那一下，之后哪怕几百万次纯读的 getInstance() 也得排队抢锁——为了 1 次写，把 999999 次读全赔了进去。能用，但亏。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">3. 饿汉式\u003C\u002Fh3>\n\u003Cp>换个思路，不躲了：类加载的时候直接造好。\"饿\"就是等不及，见到就造。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton3 instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"调用构造函数创建对象\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton3 \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getInstance\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>线程安全是白送的——JVM 保证类的初始化是排他的，一个类只初始化一次，连竞态窗口都没有。裁判数出 1 个实例。\u003C\u002Fp>\n\u003Cp>代价两个字：\u003Cstrong>浪费\u003C\u002Fstrong>。不管用不用得上，加载即占用；构造函数里若有重活（读配置、连远程），还会拖慢启动。反过来，如果这个实例大概率会用、构造也便宜，饿汉式是最省心的选择。\u003C\u002Fp>\n\u003Ch3 id=\"toc-8\">4. 双重校验锁（DCL）\u003C\u002Fh3>\n\u003Cp>前三版的合体改良：要懒加载（学第一版），要锁（学第二版），但只锁创建那一下（补第二版的亏）。这是面试高频题，两个问题必须答得上来。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton4\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> volatile\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton4 instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton4\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"调用构造函数创建对象\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton4 \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getInstance\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">        \u002F\u002F标记1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">            \u002F\u002F标记2\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            synchronized\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Singleton4.class) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">                \u002F\u002F标记3\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">                if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    instance \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton4\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">                }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">        \u002F\u002F标记4\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> instance;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>第一问：为什么要判两次 null？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n \u003Cli>标记 1 的外层判断是性能闸门：实例造好之后，绝大多数调用在这里就返回了，根本碰不到锁；\u003C\u002Fli>\n \u003Cli>标记 3 的内层判断是安全闸门：两个线程可以同时通过外层判断（都看到了 null），先到的抢到锁、造好实例、释放锁，后到的这时才进锁——如果不再检查一次 null，它会再造一个。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>一句话：\u003Cstrong>外层挡重复读，内层挡重复造\u003C\u002Fstrong>，一层都不能少。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>第二问：为什么要 volatile？\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>instance = new Singleton4() 不是一步到位，大致三步：分配内存、执行构造初始化对象、把引用赋给 instance。编译器和 CPU 可能把后两步重排成\"先赋引用、再初始化\"。重排之后，另一个线程在外层判断处看到 instance 已经非 null，兴冲冲拿走——拿到的是\u003Cstrong>还没初始化完的半成品\u003C\u002Fstrong>。volatile 禁掉这个重排，保证别人看到引用时，对象一定造完了。\u003C\u002Fp>\n\u003Ch3 id=\"toc-9\">5. 静态内部类\u003C\u002Fh3>\n\u003Cp>DCL 又难写又难讲。有没有安全 + 懒加载兼得、还不碰锁的写法？有——把实例挪进静态内部类：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton5\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> SingletonInner\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton5 INSTANCE \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton5\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton5\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"调用构造函数创建对象\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Singleton5 \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getInstance\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> SingletonInner.INSTANCE;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>两步看懂：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>外部类加载时，SingletonInner 不会跟着初始化（JVM 的类加载按需触发）——\u003Cstrong>懒加载达成\u003C\u002Fstrong>；\u003C\u002Fli>\n \u003Cli>第一次调 getInstance() 才触发内部类加载，而类初始化的排他性由 JVM 保证——\u003Cstrong>线程安全白送\u003C\u002Fstrong>，靠山和饿汉式是同一个。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>等于借 JVM 的类加载机制，把 DCL 手写的整套逻辑免费领了一遍，代码还短一半。要懒加载又怕写错 DCL，选它。\u003C\u002Fp>\n\u003Ch3 id=\"toc-10\">6. 枚举\u003C\u002Fh3>\n\u003Cp>《Effective Java》推荐的写法，一共三行：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> enum\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Singleton6\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    INSTANCE\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>写法最短，保险却最厚，一口气三重：\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>线程安全\u003C\u002Fstrong>：枚举实例在类初始化时创建，JVM 保证；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>防反射攻击\u003C\u002Fstrong>：JVM 明确禁止反射调用枚举构造器，newInstance() 直接抛异常；\u003C\u002Fli>\n \u003Cli>\u003Cstrong>防序列化破坏\u003C\u002Fstrong>：普通单例序列化再反序列化会造出新实例（得额外实现 readResolve() 才堵得住），枚举天然免疫，反序列化拿到的还是原来那个。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>反射和序列化这两个\"高级破坏手段\"，前五种写法全是裸奔，只有枚举全防住了。\u003C\u002Fp>\n\u003Cp>**六种写法之外，再看一个细节。**回头看六版 getInstance() 的返回类型：始终是具体的 Singleton1、Singleton2……调用方 import 的是具体类，依赖的也是具体类。哪天想换实现、或测试时想塞个替身进去，就得把每个调用点翻出来改——上一篇速览的六大原则里，\u003Cstrong>依赖倒置\u003C\u002Fstrong>（依赖抽象，不依赖具体实现）在这里轻轻响了声警报。单例自己解决不了这个问题，先把这句话记在小本本上，㉞ 收官篇回头细算。\u003C\u002Fp>\n\u003Ch2 id=\"toc-11\">五、六种写法怎么选\u003C\u002Fh2>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>写法\u003C\u002Fth>\n   \u003Cth>懒加载\u003C\u002Fth>\n   \u003Cth>线程安全\u003C\u002Fth>\n   \u003Cth>高并发性能\u003C\u002Fth>\n   \u003Cth>防反射\u002F序列化\u003C\u002Fth>\n   \u003Cth>一句话点评\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>懒汉式（不安全）\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>反面教材，别进生产\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>懒汉式（方法加锁）\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>能用，但每次都排队\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>饿汉式\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>简单粗暴，构造不重就选它\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>双重校验锁\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>面试必考的工业级懒加载\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>静态内部类\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>常规首选，DCL 的平替\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>枚举\u003C\u002Ftd>\n   \u003Ctd>✗\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>✓\u003C\u002Ftd>\n   \u003Ctd>Effective Java 之选\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>裁判对 2～5 版的判决是一致的，构造函数都只跑一次：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 第二版 懒汉式（方法加锁）：20 个线程同时起跑 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>调用构造函数创建对象\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>拿到 1 个不同的实例\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第六版直接比较两个引用：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 第六版 枚举 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>两次获取是否同一个实例：true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>实战建议很朴素：\u003Cstrong>不介意饿、构造便宜——第三版；要懒加载——第五版；担心反射或序列化搞破坏——第六版\u003C\u002Fstrong>。第一版永远别进生产代码；第二版留给性能无所谓的老代码；第四版——把它当成一道必须会做的面试题。\u003C\u002Fp>\n\u003Ch2 id=\"toc-12\">写在最后\u003C\u002Fh2>\n\u003Cp>六种写法翻完，主线只有一条：\u003Cstrong>线程安全\u003C\u002Fstrong>。从第一版随机的 7、3、11、5 个实例，到 DCL 的两层判断加 volatile，再到静态内部类和枚举干脆把安全外包给 JVM——所有演进都在回答同一个问题：怎么让\"只有一个实例\"在并发下依然成立。\u003C\u002Fp>\n\u003Cp>示例代码在 design-pattern 项目的 singleton 包里：Singleton1 到 Singleton6 是六种写法，SingletonTest 是那个 20 线程的裁判，跑一次 main 就能看到竞态现场——顺便说，每次跑出来的数都不一样。\u003C\u002Fp>\n\u003Cp>单例管的是\"一个类有几个实例\"，但创建型家族真正的主场是另一件事：\u003Cstrong>实例从哪来\u003C\u002Fstrong>。一堆同族对象，怎么造才不至于让调用方把 new 写得到处都是、改一个产品名就全局翻车？下一篇——\u003Cstrong>工厂模式\u003C\u002Fstrong>。\u003C\u002Fp>",[105,108,111,114,117,121,124,127,130,133,136,139],{"id":106,"text":107,"level":100},"toc-1","一、什么是单例模式",{"id":109,"text":110,"level":100},"toc-2","二、优缺点",{"id":112,"text":113,"level":100},"toc-3","三、应用场景",{"id":115,"text":116,"level":100},"toc-4","四、实现方式：六种写法",{"id":118,"text":119,"level":120},"toc-5","1. 懒汉式（线程不安全）",3,{"id":122,"text":123,"level":120},"toc-6","2. 懒汉式（方法加锁）",{"id":125,"text":126,"level":120},"toc-7","3. 饿汉式",{"id":128,"text":129,"level":120},"toc-8","4. 双重校验锁（DCL）",{"id":131,"text":132,"level":120},"toc-9","5. 静态内部类",{"id":134,"text":135,"level":120},"toc-10","6. 枚举",{"id":137,"text":138,"level":100},"toc-11","五、六种写法怎么选",{"id":140,"text":141,"level":100},"toc-12","写在最后"]