单例模式有几种写法?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,主要想考察以下几个层面,而不仅仅是让你罗列名称:

  1. 对单例模式核心思想与实现细节的理解深度:你是否真正理解 “保证一个类仅有一个实例,并提供一个全局访问点” 这一核心目标。
  2. 对多线程环境下并发安全的掌握:如何确保在高并发场景下,单例的创建依然是线程安全的,这是考察重点。
  3. 对 JVM 类加载机制、内存模型(如 volatile)、反射等底层知识的理解:不同实现方式背后的原理是什么?为何有些写法是安全的,有些则存在隐患。
  4. 实际工程应用与设计权衡能力:你是否了解每种写法的优缺点、适用场景,以及目前公认的最佳实践是什么。这能反映你的工程经验和代码品味。

核心答案

单例模式的主流写法通常可分为 5 种饿汉式懒汉式(线程不安全与同步方法版)双重检查锁(DCL)静态内部类(Holder) 以及 枚举(Enum)

安全性、简洁性、功能性综合来看,枚举(Enum) 是实现单例模式的最佳实践,它由 JVM 从根本上保证线程安全、反射安全和序列化/反序列化安全。静态内部类 则是延迟加载场景下非枚举方式的首选。

深度解析

单例模式看似简单,但 “写对” 并 “写好” 需要考虑诸多细节。下面我们逐一拆解。

1. 饿汉式(Eager Initialization)

  • 原理:在类加载的初始化阶段,就通过静态变量创建实例。这利用了 JVM 类加载机制的线程安全性来保证实例唯一。
  • 代码示例
    public class EagerSingleton {
        // 1. 私有静态常量,类加载时即初始化
        private static final EagerSingleton INSTANCE = new EagerSingleton();
        // 2. 私有构造函数
        private EagerSingleton() {}
        // 3. 公有静态方法,返回唯一实例
        public static EagerSingleton getInstance() {
            return INSTANCE;
        }
    }
    
  • 优劣分析
    • 优点:实现简单,线程安全(由 JVM 保证)。
    • 缺点非延迟加载。无论是否用到,实例都在启动时创建,若实例创建耗资源或始终未使用,则会造成资源浪费。
  • 适用场景:单例实例较小,且程序启动后立即会使用的场景。

2. 懒汉式(Lazy Initialization)

  • 基础(线程不安全)版:在 getInstance() 中判断并创建,多线程下会创建多个实例,严禁使用
    // ❌ 错误示例:线程不安全
    public static LazySingleton getInstance() {
        if (instance == null) { // 多个线程可能同时进入此判断
            instance = new LazySingleton();
        }
        return instance;
    }
    
  • 同步方法(线程安全)版:在方法上直接加 synchronized 锁。
    public class SynchronizedSingleton {
        private static SynchronizedSingleton instance;
        private SynchronizedSingleton() {}
        // 使用 synchronized 保证线程安全
        public static synchronized SynchronizedSingleton getInstance() {
            if (instance == null) {
                instance = new SynchronizedSingleton();
            }
            return instance;
        }
    }
    
    • 缺点:锁粒度太粗,每次调用 getInstance() 都需要同步,性能低下

3. 双重检查锁(Double-Checked Locking, DCL)

  • 原理:在懒汉式的基础上,将同步块的范围缩小,并在同步块内外进行两次判空检查。结合 volatile 关键字(JDK 5+)防止指令重排序导致的 “部分初始化” 问题。
  • 代码示例(标准写法)
    public class DCLSingleton {
        // 必须使用 volatile 防止指令重排
        private static volatile DCLSingleton instance;
        private DCLSingleton() {}
        public static DCLSingleton getInstance() {
            if (instance == null) { // 第一次检查,避免不必要的同步
                synchronized (DCLSingleton.class) {
                    if (instance == null) { // 第二次检查,确保唯一
                        instance = new DCLSingleton(); // 依赖 volatile 保证写操作先行发生
                    }
                }
            }
            return instance;
        }
    }
    
  • 最佳实践与常见误区
    • volatile 关键字必不可少。没有它,线程可能拿到一个未初始化完全的对象(半初始化状态)。
    • 这是延迟加载且性能较好的方案,但代码稍显复杂。

4. 静态内部类(Static Inner Class / Holder)

  • 原理:利用 JVM 的类加载机制 —— 静态内部类只有在被主动引用时(如调用 getInstance())才会加载,从而实现延迟加载。其静态变量初始化由 JVM 保证线程安全。
  • 代码示例
    public class InnerClassSingleton {
        private InnerClassSingleton() {}
        // 静态内部类
        private static class SingletonHolder {
            private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
        }
        public static InnerClassSingleton getInstance() {
            return SingletonHolder.INSTANCE; // 触发内部类加载和初始化
        }
    }
    
  • 优劣分析
    • 优点:实现简洁,线程安全,延迟加载,性能好(无锁)。是非枚举方式中的首选
    • 缺点:无法防止通过反射调用私有构造函数创建新实例(但可以通过在构造器中加判断来防御)。

5. 枚举(Enum)【最佳实践】

  • 原理:Java 枚举类型本身的特性保证了其常量(即单例实例)在 JVM 中是唯一的,且其构造器是私有的。JVM 从根本上保证了线程安全、反射安全(JDK 内部禁止使用反射创建枚举实例)和序列化安全(枚举的序列化机制特殊,仅存储名字,反序列化时通过 valueOf 获取同名常量)。
  • 代码示例
    public enum EnumSingleton {
        INSTANCE; // 单例实例
        // 可以添加任意方法和属性
        public void doSomething() {
            System.out.println("Singleton Business Method.");
        }
    }
    // 使用:EnumSingleton.INSTANCE.doSomething();
    
  • 最佳实践
    • 《Effective Java》作者 Josh Bloch 强烈推荐的方式
    • 代码极度简洁,且能抵御反射和序列化的攻击,是功能最完备的单例实现。

总结

实现单例模式的关键在于确保线程安全、实现延迟加载(按需创建)并防御反射与序列化破坏。在无特殊需求时,优先使用枚举(Enum)实现;若因历史原因不能使用枚举,静态内部类(Holder) 是实现延迟加载单例的优秀选择。