第058篇 单例模式六种写法:线程安全与懒加载的平衡

简介: 单例模式是面试高频考点,六种写法各具特点:饿汉式线程安全但非懒加载;懒汉式需同步;DCL需`volatile`防重排;静态内部类最推荐(JVM类初始化锁保障懒加载与线程安全);枚举天然防反射与序列化。Android中优先用静态内部类,持Context时务必用`getApplicationContext()`,避免内存泄漏。

单例模式是设计模式里最容易被追问到底的一个。它的写法有六种,每种都能延伸出三四个追问:懒加载怎么保证线程安全、双重检查锁为什么要加 volatile、静态内部类是什么时候初始化的、枚举为什么天然防反射与序列化、单例持有 Context 怎么避免泄漏。答到第五层的人不多,但这一层恰恰是区分"看过书"和"踩过坑"的地方。

先把结论放在前面:单例的核心约束是私有构造 + 全局唯一访问点 + 唯一实例保证。 六种写法按"实例创建时机"与"线程安全手段"两个维度排列:饿汉式(类加载即建)、懒汉式(用时才建,需同步)、双重检查锁(懒加载 + 局部加锁 + volatile)、静态内部类(类加载时懒初始化,靠 JVM 类初始化锁保证)、静态内部类 + 双重检查锁(极端场景加固)、枚举(由 JVM 保证唯一且防反射反序列化)。 Android 上的推荐选择:不关心 Context 的纯工具用静态内部类;需要 Context 的用 Application 本身或持 getApplicationContext();能用 DI 容器管理的就别硬写单例。

机制拆解

每种写法的差异,根源都在三个机制上。第一,类初始化锁。Java 规范保证类的静态初始化(static 字段赋值、静态块)只会被执行一次,并且是线程安全的——同一时刻只有一个线程执行初始化,其他线程阻塞等待。 静态内部类的单例正是靠这个:Holder 类没人访问时不会被初始化,第一次 getInstance() 触发初始化,JVM 保证这一步的原子性,于是既实现了懒加载又不用自己加锁。这是最推荐的一种写法,因为它把并发问题交给了语言规范。

第二,Java 内存模型与指令重排。构造对象在字节码层面是"分配内存 → 赋初值 → 调用构造方法",JIT 与 CPU 都可能重排。若没有 volatile,另一个线程可能看到"引用非空但对象还没构造完"的半初始化状态,于是拿到一个字段全为默认值的对象。加上 volatile 后,写入引用前的动作不会被重排到引用写入之后,读到引用就能看到完整状态。

第三,JVM 对单例的破坏手段。setAccessible(true) 能让私有构造被反射调用,于是 newInstance() 出第二个实例;反序列化也会绕过构造方法重建对象。枚举因为由 JVM 在类加载时创建、无法反射重建(连 name 字段都是 final 反射改不了)、反序列化返回缓存实例,所以被《Effective Java》推荐。

这些坑的正确绕法

最常见的坑是双重检查锁漏掉 volatile,导致别的线程拿到"半初始化"对象。 表现是偶发的 NullPointerException,堆栈指向构造函数里还没执行到的字段,字段值是 null 或 0。这类 bug 在低并发下不出现,测试环境几乎抓不到,上线后在高并发场景才暴露,排查成本极高。 修法很简单:加 volatile,但很多人不知道它为什么必须加——面试时能解释清楚"重排导致引用先发布",这题就过关了。

其次是单例持有 Activity/View 造成内存泄漏。 静态字段引用了界面对象,引用链从静态区一直连到整棵视图树,GC 根可达,于是界面销毁后对象仍然驻留;连续打开详情页,内存曲线呈阶梯上升。 对策有三条:单例只持 Application;需要绑定界面时用 WeakReference 并在 onDestroy 里解除订阅;把这类状态放到 ViewModel 里,随作用域自动清理。

还有一个更隐蔽的坑:静态单例里的状态跨页面共享,导致数据串台。 比如一个 UserCache 单例在登录后被清空,但某个界面还持有了旧数据的引用;或者用户在 A 页面修改资料后,B 页面因单例缓存没刷新而显示旧值。 Android 上更妥当的做法是作用域明确的状态容器:ViewModel 管界面级、Singleton 管应用级、Activity 作用域的缓存用 onSaveInstanceState 或直接随生命周期释放。能用作用域解决的状态,不要塞进单例。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

// 1) 饿汉式:类加载即创建,线程安全,但不用也占内存
public class Eager {
    private static final Eager I = new Eager(); private Eager(){
   }
    public static Eager get() {
    return I; } }

// 2) 懒汉式(线程不安全):用时才建,并发下会建出多个实例
//    private static Lazy inst; public static synchronized void check() {
    if (inst == null) inst = new Lazy(); }

// 3) 懒汉式 + synchronized:安全,但每次获取都加锁
public class SyncLazy {
    private static SyncLazy inst;
    private SyncLazy(){
   }
    public static synchronized SyncLazy get() {
   
        return inst == null ? (inst = new SyncLazy()) : inst; } }

// 4) 双重检查锁:volatile 不可省,省了会拿到半初始化对象
public class Dcl {
   
    private static volatile Dcl inst;                    // 重排屏障,保证发布有序
    private Dcl(){
   }
    public static Dcl get() {
   
        Dcl local = inst;                                 // 局部变量,减少一次读
        if (local == null) synchronized (Dcl.class) {
   
            local = inst;
            if (local == null) local = inst = new Dcl();  // 二次检查
        }
        return local;
    }
}

// 5) 静态内部类:Android 推荐写法,懒加载 + 靠类初始化锁保证唯一
public class Holder {
   
    private Holder(){
   }
    private static class Inner {
    private static final Holder I = new Holder(); }
    public static Holder get() {
    return Inner.I; }         // 首次调用才加载 Inner
}

// 6) 枚举:JVM 保证唯一,且防反射与反序列化破坏
public enum EnumSingleton {
   
    INSTANCE;
    public void doWork() {
    }
}
// EnumSingleton.INSTANCE.doWork();

这段代码值得盯三处:第一处,DCL 里 volatile 与两次检查缺一不可,局部变量则是为了减少 volatile 读并配合后续链式调用;第二处,静态内部类的懒初始化靠"Inner 类第一次被引用时才初始化"这个 JVM 语义,比手写同步更简洁;第三处,枚举式的独特之处不在于懒加载,而在于无法被反射或反序列化创建出第二个实例。

这题在面试里怎么问、怎么答

"六种写法有什么区别,怎么选?"给一张对照表思路:创建时机(类加载 / 首次调用)、同步方式(无 / synchronized / 类初始化锁 / JVM 强制)、是否防反射破坏(只有枚举是)、是否可继承(枚举与静态内部类可被继承实现,饿汉式可继承但私有构造会挡住子类)、使用成本。结论落在选择:默认用静态内部类,需要防序列化与反射就枚举,需要 Context 的单例要额外约束持有对象。

"静态内部类为什么是线程安全的?"答:JVM 规范保证类的静态初始化过程只执行一次且线程安全,其他线程阻塞等待初始化完成。所以 Inner.I 的赋值只有一次,天然可见。

"为什么加 volatile 就能避免半初始化?"答:对象构造在字节码层是"分配内存 → 赋默认值 → 执行构造",没有内存屏障时 CPU 与 JIT 可能重排,导致引用先于内容发布;volatile 的写语义(写之前的操作不被重排到写之后)加上读语义(读到新值能看到此前所有写入),建立了 happens-before 关系。

"Android 上 Application 算不算单例,怎么用?"答:进程内系统只创建一个 Application 实例,天然全局唯一;在 AndroidManifest 声明后由框架创建,attachBaseContext 之后可用;自定义 MyApp extends Application 是标准做法,第三方 SDK 的初始化都放在这里。 也可以用 ViewModelProvider.AndroidViewModelFactory 这类官方机制拿 Context。注意多进程场景下每个进程各有自己的 Application 实例,跨进程共享要用 ContentProvider 或独立服务。

"使用中遇到过什么问题?"案例一:静态 AppContext 字段误写成 Activity 引用,详情页反复打开后内存不降;定位到 getInstance(Context) 里直接存了传入的 Context;修复为统一 getApplicationContext()。 案例二:单例里的缓存用户在退出登录后没清,下一个账号短暂看到上一个账号的数据;定位到 UserCache 是进程级状态;修复为把登录态缓存改成随登录域管理的实例,或在登出时显式清理。

再补一个工程上值得讲清的点:Android 上真正的"全局单例"不止是 Java 单例。 ContentProvider 在应用启动早期(Application 之前)初始化,常用来做跨进程初始化的入口;WorkManager 自己就是一个内部单例式的调度中心;Service 同样是跨组件的全局存在。 理解这些系统级"天然单例"和用 object 写单例的区别,才能在架构讨论时不把两件事混为一谈。另外要记住一条纪律:单例负责"唯一",不负责"万能"。往单例里塞状态、工具、缓存三样东西,测试就无法替换、生命周期无法管理,这是 Android 项目里最常见的"上帝类"来源。

给正在准备面试的你

把这题画成一张二维表:横轴是"创建时机"(类加载时 / 首次调用时),纵轴是"同步手段"(无 / synchronized / volatile + 局部锁 / JVM 类初始化锁 / JVM 强制唯一)。 把六种写法放进对应格子并标上关键缺陷——饿汉式"不用也占内存"、懒汉式"并发建多个"、同步版"每次加锁"、DCL"漏 volatile 出半初始化"、静态内部类"推荐"、枚举"防反射但不能懒加载"。表格下方画一条线,写"Android 默认选静态内部类;需要防反射/序列化选枚举;能用 DI 就不手写"。这张表在面试里说三分钟,比罗列二十个名字有用。

再补工程案例与踩坑——应用落点是审查项目里所有静态字段,确认是否捕获了界面上下文、状态是否有明确的作用域;把工具类里的缓存拆出去交给 DI 或 ViewModel;对确实需要全局唯一的对象补上 Application 而非 Activity 的约束。

复习时别孤立刷题:设计模式入门——单例是创建型里最常被追问的,策略与工厂才是 Android 里性价比更高的两个。

划两句重点:默认用静态内部类(懒加载 + JVM 保证唯一);DCL 的 volatile 不能省,原因是引用先发布导致半初始化;枚举唯一真正防反射与反序列化;单例只持 Application,状态作用域别乱塞。

下一篇聊建造者模式:链式调用为何无处不用在哪些地方——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:设计模式入门:单例、工厂、观察者的-Android-落地

下一篇预告:建造者模式:链式调用为何无处不用在哪些地方

有任何问题欢迎在评论区留言交流。

相关文章
|
17天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8478 24
|
16天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2817 14
|
15天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
2021 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
14天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
10天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
4天前
|
人工智能 JSON Linux
【全网最详细】ComfyUI使用教程:下载+本地部署+配置+工作流搭建一篇搞定(2026最新版)
ComfyUI是一款免费开源的本地AI绘图工具,采用节点式工作流设计,支持文生图、图生图、局部重绘、放大、换脸等多种功能。可离线运行,依赖显卡加速,无需联网。支持自定义流程保存与分享,插件生态丰富,适合进阶用户。(239字)
|
10天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)

热门文章

最新文章