4、(创建型设计模式)Java 单例模式深度学习指南

以下是为你精心撰写的 《Java 单例模式深度学习指南》,专为 Java 后端开发者设计,内容涵盖:定义、作用、实际场景、实现方式对比、避坑指南、最佳实践、Spring 中的单例、面试高频题,所有示例均含中文注释,可直接用于项目开发与面试准备。


📘 Java 单例模式深度学习指南

—— 从零掌握“全局唯一实例”的设计艺术

作者:Java 后端架构实战导师
适用对象:Java 后端开发者(Spring Boot / 微服务 / 分布式系统)
目标:彻底理解单例模式,不再“只会写代码”,而是“懂原理、会选型、能避坑”
核心原则不是所有全局变量都叫单例,不是所有单例都线程安全


✅ 一、什么是单例模式?

📌 定义:

单例模式(Singleton Pattern) 是一种创建型设计模式,它确保一个类在整个 JVM(Java 虚拟机)生命周期中只有一个实例,并提供一个全局访问点来获取这个唯一实例。

🔍 核心思想:

  • 唯一性:整个系统中,该类只能被创建一次。
  • 懒惰/立即创建:可以选择在类加载时创建,或在第一次使用时创建。
  • 全局访问:提供一个静态方法(如 getInstance())让任何地方都能获取该实例。

💡 一句话记忆
“一个类,一个对象,全局可访问。”


✅ 二、单例模式有什么作用?(Why Use Singleton?)

作用说明
节省资源避免重复创建开销大的对象(如数据库连接、线程池、Redis 客户端)
全局共享状态所有模块操作的是同一个对象,保证数据一致性(如配置中心、日志器)
控制实例数量防止滥用资源(如文件句柄、网络连接)
简化配置管理配置信息只需加载一次,避免重复读取配置文件
提高性能减少对象创建与垃圾回收压力

⚠️ 注意:单例不是“全局变量”!

  • 全局变量是 public static 字段,无封装、无控制
  • 单例是封装+控制+延迟创建的完整设计模式。

✅ 三、单例模式的典型使用场景(Java 后端真实案例)

场景说明是否推荐使用单例
✅ 数据库连接池HikariCP、Druid 等连接池必须唯一,避免连接数爆炸✅ 强烈推荐
✅ Redis 客户端Jedis、Lettuce 客户端应全局共享,避免频繁创建 TCP 连接✅ 推荐
✅ 日志记录器SLF4J 的 LoggerFactory.getLogger() 本质是单例✅ 推荐
✅ 配置管理器读取 application.yml 并缓存配置,避免重复解析✅ 推荐
✅ UUID 生成器使用 java.util.UUID.randomUUID() 无需单例,但自定义生成器可封装⚠️ 可选
✅ 系统时间服务System.currentTimeMillis() 无需封装,但业务时间戳生成器可封装⚠️ 按需
❌ 工具类(如 StringUtils无状态,无需实例,静态方法即可❌ 不推荐
❌ 业务实体(如 UserOrder每个用户/订单都应独立❌ 绝对禁止

判断标准
“这个对象是否昂贵?是否需要共享状态?是否只能有一个?”
→ 三者都满足 → 用单例!


✅ 四、单例模式的五种实现方式详解(含中文注释)

我们从最简单最严谨,逐步分析每种写法的优缺点。


🔹 1. 饿汉式(Eager Initialization)—— 类加载时创建

/**
 * 饿汉式单例(推荐用于简单场景)
 * 特点:类加载时就创建实例,线程安全,但可能浪费内存
 */
public class DatabaseConnectionPool {
    // 1. 私有静态实例:JVM 类加载时自动创建(线程安全)
    private static final DatabaseConnectionPool INSTANCE = new DatabaseConnectionPool();

    // 2. 私有构造函数:禁止外部 new
    private DatabaseConnectionPool() {
        System.out.println("✅ 数据库连接池已初始化(饿汉式)");
        // 可在此初始化连接池、加载配置等
    }

    // 3. 公共静态方法:提供全局访问点
    public static DatabaseConnectionPool getInstance() {
        return INSTANCE; // 直接返回,无需同步
    }

    // 模拟获取连接
    public void getConnection() {
        System.out.println("🔗 获取数据库连接...");
    }
}

// 使用示例
public class EagerSingletonDemo {
    public static void main(String[] args) {
        DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
        DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();

        System.out.println(pool1 == pool2); // true,证明是同一对象
        pool1.getConnection(); // 输出:🔗 获取数据库连接...
    }
}
✅ 优点:
  • 实现简单,线程安全(JVM 类加载机制保证)
  • 获取速度快(无锁)
❌ 缺点:
  • 类加载即创建,即使从未使用,也会占用内存(浪费资源)
  • 无法控制初始化时机
💡 适用场景:

对象创建成本低、必用、启动即需:如日志器、配置中心、系统监控器


🔹 2. 懒汉式(Lazy Initialization)—— 第一次使用时创建(线程不安全版)

/**
 * 懒汉式单例(线程不安全版本)—— ❌ 错误示范!
 * 特点:延迟创建,节省资源,但多线程下会创建多个实例
 */
public class BadSingleton {
    // 1. 声明静态实例,初始为 null
    private static BadSingleton instance;

    // 2. 私有构造函数
    private BadSingleton() {
        System.out.println("✅ 懒汉式单例被创建");
    }

    // 3. 公共静态方法:首次调用时才创建
    public static BadSingleton getInstance() {
        if (instance == null) { // 👈 多线程同时进入这里,会创建多个实例!
            instance = new BadSingleton();
        }
        return instance;
    }
}

// 使用示例(多线程测试)
public class BadSingletonDemo {
    public static void main(String[] args) {
        // 模拟10个线程同时获取实例
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                BadSingleton s = BadSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + " -> " + s);
            }).start();
        }
        // 输出:可能看到多个不同对象!严重错误!
    }
}
❌ 问题:

线程不安全! 在高并发下,多个线程同时进入 if (instance == null),会创建多个实例 → 破坏单例!

⚠️ 结论:

绝对不要在生产环境中使用此版本!


🔹 3. 懒汉式 + 同步方法(Synchronized Method)—— 线程安全但性能差

/**
 * 懒汉式 + 同步方法(线程安全,但效率低)
 * 特点:每次调用 getInstance() 都加锁,即使实例已创建
 */
public class SynchronizedSingleton {
    private static SynchronizedSingleton instance;

    private SynchronizedSingleton() {
        System.out.println("✅ 懒汉式 + 同步方法被创建");
    }

    // 1. 整个方法加锁(synchronized)
    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

// 使用示例
public class SyncSingletonDemo {
    public static void main(String[] args) {
        // 多线程测试
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                SynchronizedSingleton s = SynchronizedSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + " -> " + s);
            }).start();
        }
        // 输出:只会创建一次,但每个线程都阻塞等待锁
    }
}
✅ 优点:
  • 线程安全,解决了多线程创建多个实例的问题
❌ 缺点:
  • 性能极差:每次调用都加锁,即使实例已创建,也要排队等待
  • 锁粒度太大,不符合“只在创建时加锁”的原则
💡 适用场景:

仅用于学习或低并发场景生产环境不推荐


🔹 4. 双重检查锁定(Double-Checked Locking)—— 推荐写法(JDK 5+)

/**
 * 双重检查锁定(DCL)单例 —— ✅ 推荐生产环境使用
 * 特点:延迟创建 + 线程安全 + 性能高
 */
public class DCLSingleton {
    // 1. 使用 volatile 关键字!防止指令重排序
    private static volatile DCLSingleton instance;

    private DCLSingleton() {
        System.out.println("✅ 双重检查锁定单例被创建");
    }

    // 2. 双重检查 + 同步块
    public static DCLSingleton getInstance() {
        // 第一次检查:避免不必要的同步(提高性能)
        if (instance == null) {
            // 加锁:只在创建时同步
            synchronized (DCLSingleton.class) {
                // 第二次检查:防止多个线程同时通过第一次检查
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

// 使用示例
public class DCLSingletonDemo {
    public static void main(String[] args) {
        // 多线程并发测试
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                DCLSingleton s = DCLSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + " -> " + s);
            }).start();
        }
        // 输出:只会创建一次,且无性能损耗
    }
}
✅ 优点:
  • 延迟初始化:第一次调用才创建
  • 线程安全:双重检查 + synchronized
  • 高性能:创建后不再加锁
  • 内存可见性volatile 确保多线程看到最新值
⚠️ 关键点:必须加 volatile

如果没有 volatile,JVM 可能发生指令重排序

  1. 分配内存
  2. 调用构造函数
  3. 将引用赋值给 instance
    → 如果重排序为 1→3→2,其他线程可能拿到一个未初始化完成的对象 → 崩溃!
💡 适用场景:

高并发、延迟加载、资源昂贵:如 Redis 客户端、数据库连接池、消息队列连接器

这是 Java 8+ 后最推荐的手写单例方式!


🔹 5. 静态内部类(Static Inner Class)—— 最优雅的实现(推荐!)

/**
 * 静态内部类单例 —— ✅ 最推荐的写法(无锁、延迟、线程安全)
 * 利用 JVM 类加载机制保证线程安全
 */
public class StaticInnerSingleton {
    // 1. 私有构造函数
    private StaticInnerSingleton() {
        System.out.println("✅ 静态内部类单例被创建");
    }

    // 2. 静态内部类:只有在调用 getInstance() 时才会加载
    private static class SingletonHolder {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    // 3. 获取实例:调用时才加载内部类
    public static StaticInnerSingleton getInstance() {
        return SingletonHolder.INSTANCE; // JVM 保证线程安全
    }
}

// 使用示例
public class StaticInnerSingletonDemo {
    public static void main(String[] args) {
        // 多线程并发测试
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                StaticInnerSingleton s = StaticInnerSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + " -> " + s);
            }).start();
        }
        // 输出:只会创建一次,且无锁、无延迟、线程安全
    }
}
✅ 优点:
  • 线程安全:JVM 类加载机制天然保证
  • 延迟加载:第一次调用 getInstance() 才加载内部类
  • 无锁:性能极高
  • 代码简洁:无 volatile、无 synchronized
  • 符合 Java 最佳实践
💡 为什么安全?

Java 虚拟机规范规定:类的初始化是线程安全的
SingletonHolder 只有在 getInstance() 被调用时才会被加载和初始化,且由 JVM 保证只初始化一次。

结论

这是目前最推荐的单例写法!
面试、项目、开源框架(如 Spring)中广泛使用!


🔹 6. 枚举单例(Enum Singleton)—— 最安全的实现(终极推荐)

/**
 * 枚举单例 —— ✅ 最安全、防反射、防序列化、最推荐的终极方案
 * Java 语言特性保证:枚举是天生的单例
 */
public enum EnumSingleton {
    INSTANCE;

    // 可添加业务方法
    public void doSomething() {
        System.out.println("✅ 枚举单例正在执行任务...");
    }

    // 可存储状态
    private String config = "默认配置";

    public String getConfig() {
        return config;
    }

    public void setConfig(String config) {
        this.config = config;
    }
}

// 使用示例
public class EnumSingletonDemo {
    public static void main(String[] args) {
        // 获取实例
        EnumSingleton singleton = EnumSingleton.INSTANCE;

        // 调用方法
        singleton.doSomething(); // ✅ 枚举单例正在执行任务...

        // 修改状态
        singleton.setConfig("新配置");
        System.out.println("当前配置:" + singleton.getConfig());

        // 多线程测试
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                EnumSingleton s = EnumSingleton.INSTANCE;
                System.out.println(Thread.currentThread().getName() + " -> " + s);
            }).start();
        }
    }
}
✅ 优点:
  • 绝对线程安全
  • 防止反射攻击Enum 构造器是私有的,反射也无法创建新实例)
  • 防止序列化破坏ObjectInputStream 读取枚举时会返回原实例)
  • 代码最简洁
  • JVM 保证唯一性
❌ 缺点:
  • 不支持延迟加载(类加载时就创建)
  • 无法继承(枚举不能继承其他类)
💡 适用场景:

要求绝对安全、防破解、防反序列化:如安全令牌、加密服务、支付网关配置

Joshua Bloch(《Effective Java》作者)推荐:优先使用枚举单例!


✅ 五、单例模式的避坑指南(Java 后端高频踩坑点)

问题原因解决方案
❌ 反射破坏单例使用 Constructor.setAccessible(true) 创建新实例✅ 使用枚举单例,或在构造器中抛异常:if (instance != null) throw new RuntimeException("禁止反射创建!")
❌ 序列化破坏单例ObjectInputStream.readObject() 会创建新对象✅ 实现 readResolve() 方法返回原实例
❌ 多 ClassLoader 破坏单例Web 容器中多个应用加载同一个类✅ 用 Spring 容器管理单例,避免手写单例
❌ 多 JVM 破坏单例分布式系统中每个 JVM 都有独立实例✅ 单例只在单 JVM 生效!分布式用 Redis / ZooKeeper 实现全局唯一
❌ 静态变量被误改public static 暴露实例✅ 私有化 + getInstance() 封装

🔧 防反射破坏示例(非枚举写法):

public class SafeSingleton {
    private static volatile SafeSingleton instance;

    private SafeSingleton() {
        // 防止反射创建
        if (instance != null) {
            throw new RuntimeException("请使用 getInstance() 方法获取实例!");
        }
    }

    public static SafeSingleton getInstance() {
        if (instance == null) {
            synchronized (SafeSingleton.class) {
                if (instance == null) {
                    instance = new SafeSingleton();
                }
            }
        }
        return instance;
    }

    // 防止序列化破坏
    private Object readResolve() {
        return getInstance();
    }
}

✅ 六、Spring 框架中的单例模式(你每天都在用!)

在 Spring 中,默认所有 Bean 都是单例

@Component  // ✅ 默认作用域是 singleton
public class RedisClient {
    private String host = "localhost";

    public void connect() {
        System.out.println("🚀 连接到 Redis:" + host);
    }
}

@Service
public class OrderService {
    @Autowired
    private RedisClient redisClient; // 每次注入的都是同一个对象!

    public void createOrder() {
        redisClient.connect(); // 调用的是同一个实例
    }
}

@Configuration
public class AppConfig {
    @Bean
    public DatabaseConnectionPool connectionPool() {
        return new DatabaseConnectionPool(); // Spring 容器管理为单例
    }
}

Spring 单例 vs 手写单例

维度手写单例Spring 单例
实例创建类加载或首次调用容器启动时创建
生命周期JVM 生命周期Spring 容器生命周期
管理方式手动控制IoC 容器自动管理
线程安全需自己保证Spring 保证
推荐度仅用于工具类✅ 业务 Bean 优先用 Spring

💡 建议
业务对象用 @Component / @Service,工具类用枚举单例,避免手写 DCL!


✅ 七、学习建议与进阶路径

阶段建议
📚 第一周EnumSingleton 替换你项目中所有手写单例(如配置类、工具类)
📚 第二周理解 volatile 为什么在 DCL 中必须使用,查阅 Java 内存模型(JMM)
📚 第三周在 Spring Boot 中,观察 @Component Bean 的创建过程(日志:BeanFactory
📚 第四周尝试用 @Scope("prototype") 改为原型模式,对比差异
📚 面试准备准备回答:

“你项目中哪里用了单例?”
“手写一个线程安全的单例?”
“为什么用枚举?”
“Spring 中的单例和手写单例区别?” |


✅ 八、总结:单例模式选型决策树

graph TD
    A[需要单例吗?] --> B{是否要求绝对安全?}
    B -->|是| C[使用枚举单例(EnumSingleton)]
    B -->|否| D{是否需要延迟加载?}
    D -->|是| E[使用静态内部类(StaticInnerSingleton)]
    D -->|否| F[使用饿汉式(EagerSingleton)]
    G[是否在 Spring 项目中?] --> H[用 @Component / @Service,让 Spring 管理]

最终推荐

  • 普通工具类枚举单例(最安全)
  • Spring 项目中@Component(最简单)
  • 非 Spring、高并发、延迟加载静态内部类
  • 绝对不要用懒汉式(无锁)或同步方法

✅ 九、附录:单例模式面试高频题(附答案)

Q1:单例模式有几种写法?推荐哪一种?

A:5种(饿汉、懒汉、同步方法、DCL、静态内部类、枚举),推荐枚举和静态内部类

Q2:为什么 DCL 要加 volatile

A:防止指令重排序导致返回未初始化对象,volatile 保证可见性和有序性。

Q3:Spring 中的单例是线程安全的吗?

A:,Spring 的 Bean 默认是单例,容器保证线程安全(但 Bean 本身如果包含可变状态,仍需线程安全设计)。

Q4:单例能被反射破坏吗?如何防止?

A:能。在构造器中加判断 if (instance != null) throw new RuntimeException(),或使用枚举。

Q5:分布式系统中能用单例吗?

A:不能!单例只在 JVM 内部唯一。分布式需用 Redis、ZooKeeper 实现全局唯一锁或配置中心。


✅ 十、结语:真正的高手,不写单例,而用 Spring

单例不是目的,而是手段。
你真正要解决的是:“如何让一个昂贵对象在整个系统中被共享且只创建一次?”

当你使用 @Autowired RedisClient 时,你已经在用单例了。
当你用 EnumSingleton 管理加密密钥时,你已经做到了极致安全。

不要为了“写单例”而写单例,
要为了“系统稳定、性能高效、资源可控”而选择它。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值