【西瓜带你学设计模式 | 第一期-单例模式】单例模式——定义、实现方式、优缺点与适用场景以及注意事项

文章目录

    • 前言
    • [1. 单例模式是什么?](#1. 单例模式是什么?)
    • [2. 为什么要用单例?能解决什么问题?](#2. 为什么要用单例?能解决什么问题?)
    • [3. 单例模式的实现思路](#3. 单例模式的实现思路)
    • [4. 饿汉式单例(线程安全,创建早)](#4. 饿汉式单例(线程安全,创建早))
      • [4.1 代码示例(核心思想:类加载即初始化)](#4.1 代码示例(核心思想:类加载即初始化))
      • [4.2 优缺点](#4.2 优缺点)
    • [5. 懒汉式单例(延迟创建,但线程不安全)](#5. 懒汉式单例(延迟创建,但线程不安全))
      • [5.1 代码示例(核心思想:第一次调用才创建)](#5.1 代码示例(核心思想:第一次调用才创建))
      • [5.2 为什么线程不安全?](#5.2 为什么线程不安全?)
    • [6. 懒汉式 + synchronized(线程安全,但性能差)](#6. 懒汉式 + synchronized(线程安全,但性能差))
      • [6.1 代码示例(方法级锁)](#6.1 代码示例(方法级锁))
      • [6.2 优缺点](#6.2 优缺点)
    • [7. 双重检查锁(Double-Checked Locking, DCL)](#7. 双重检查锁(Double-Checked Locking, DCL))
      • [7.1 代码示例(核心思想:减少同步开销)](#7.1 代码示例(核心思想:减少同步开销))
      • [7.2 为什么要 `volatile`?](#7.2 为什么要 volatile?)
      • [7.3 优缺点](#7.3 优缺点)
    • [8. 静态内部类单例(懒加载 + 线程安全)](#8. 静态内部类单例(懒加载 + 线程安全))
      • [8.1 代码示例](#8.1 代码示例)
      • [8.2 为什么它线程安全?](#8.2 为什么它线程安全?)
      • [8.3 优缺点](#8.3 优缺点)
    • [9. 枚举单例(防止"反射/序列化攻击"的方式)](#9. 枚举单例(防止“反射/序列化攻击”的方式))
      • [9.1 代码示例](#9.1 代码示例)
      • [9.2 优点](#9.2 优点)
      • [9.3 缺点](#9.3 缺点)
    • [10. 单例模式的注意事项](#10. 单例模式的注意事项)
    • [11. 单例模式的适用场景总结](#11. 单例模式的适用场景总结)
    • [12. 一句话总结:怎么选实现方式?](#12. 一句话总结:怎么选实现方式?)

前言

在软件开发中,"某个类只应该有一个实例,并且提供全局访问点"是非常常见的需求。单例模式就是为了解决这个问题:保证对象唯一 ,并且统一入口 访问该对象。


1. 单例模式是什么?

单例模式 :确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

常见用途:

  • 配置管理类(读取配置、全局生效)
  • 线程池/缓存管理器
  • 日志器(Logger)
  • 数据库连接池(通常已有更专业实现,但思路类似)

2. 为什么要用单例?能解决什么问题?

单例模式通常用于以下场景:

  1. 控制资源数量:比如某些昂贵资源(线程池、缓存、硬件设备等)不应该重复创建。
  2. 共享状态/统一管理:需要全局统一的管理入口。
  3. 避免重复初始化:系统启动时初始化一次即可,后续复用。

3. 单例模式的实现思路

从实现方式看,常见单例分为三大类:

  1. 饿汉式(类加载时就创建)
  2. 懒汉式(第一次使用才创建)
  3. 线程安全懒汉式(如:双重检查锁 DCL、静态内部类等)

此外还有:

  • 枚举单例(Java 中最推荐之一的写法,天然防反序列化破坏)
  • 注册式/容器式单例(更灵活,但不属于经典 GoF 单例)

4. 饿汉式单例(线程安全,创建早)

4.1 代码示例(核心思想:类加载即初始化)

java 复制代码
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

4.2 优缺点

优点

  • 天然线程安全:JVM 类加载机制保证初始化过程是安全的
  • 实现简单

缺点

  • 如果程序运行中从未用到这个单例,会造成不必要的资源占用
  • 可能导致类加载变慢(在某些场景)

适用场景

  • 单例创建成本不高
  • 或者几乎确定程序会用到该单例

5. 懒汉式单例(延迟创建,但线程不安全)

5.1 代码示例(核心思想:第一次调用才创建)

java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

5.2 为什么线程不安全?

在多线程环境下可能出现:

  • 线程 A 判断 instance == null,准备创建
  • 线程 B 也判断 instance == null,也创建
  • 最终产生多个实例

因此:懒汉式要想正确,就必须处理并发。


6. 懒汉式 + synchronized(线程安全,但性能差)

6.1 代码示例(方法级锁)

java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

6.2 优缺点

优点

  • 线程安全,逻辑清晰

缺点

  • 每次调用 getInstance() 都要获得锁,性能较差
  • 高并发下锁竞争明显

适用场景

  • 并发不高、对性能要求不高的场景(但一般不推荐)

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

7.1 代码示例(核心思想:减少同步开销)

java 复制代码
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查:减少锁获取次数
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查:防止并发创建
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

7.2 为什么要 volatile

关键点在于指令重排问题:

  • 没加 volatile 时,可能发生这样的顺序:
    1. 分配内存
    2. 将内存地址赋给 instance
    3. 调用构造函数初始化对象

此时另一个线程可能看到 instance != null,但对象还没初始化完成。

volatile 能禁止关键重排,并保证可见性。

详细了解volatile相关可以看我的另一篇文章 volatile的三大特性、底层原理

7.3 优缺点

优点

  • 懒加载
  • 高并发下性能比 synchronized 方案好

缺点

  • 代码更复杂
  • 容易漏掉 volatile

适用场景

  • 需要懒加载 + 高并发性能

8. 静态内部类单例(懒加载 + 线程安全)

8.1 代码示例

java 复制代码
public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

8.2 为什么它线程安全?

  • Holder 类只有在第一次调用 getInstance() 时才会被加载
  • JVM 类加载过程是线程安全的
  • 因此能保证 INSTANCE 只初始化一次

8.3 优缺点

优点

  • 懒加载
  • 线程安全
  • 代码简洁,不需要 synchronized / volatile

缺点

  • 初学者可能不理解"静态内部类触发加载"的机制

适用场景

  • 通常工程里这是非常推荐的实现方式

9. 枚举单例(防止"反射/序列化攻击"的方式)

9.1 代码示例

java 复制代码
public enum Singleton {
    INSTANCE;
    public void someMethod() {}
}

9.2 优点

  • 天然支持线程安全
  • 防止反射创建新实例(enum 的机制更可靠)
  • 防止反序列化破坏单例(JVM 语义保证)

9.3 缺点

  • 不能继承其他类(enum 本身限制)
  • 用法风格不同于常规类

适用场景

  • 用于"必须绝对唯一"的单例场景

10. 单例模式的注意事项

  1. 反射破坏单例

    • 普通单例(尤其懒汉式/饿汉式)可能被反射绕过
    • 枚举单例更安全
  2. 序列化破坏单例

    • 如果实现了 Serializable,需要考虑 readResolve() 保证反序列化返回同一个实例
    • enum 单例可避免很多问题
  3. 多线程可见性与指令重排(DCL)

    • DCL 必须加 volatile(这是最常见错误)
  4. 单例不是越多越好

    • 单例会带来全局状态,滥用可能引发可维护性、测试困难、隐式耦合
  5. 是否真的需要单例?

    • 很多场景其实是"全局可访问的对象",但不一定要严格"唯一实例"
    • 例如依赖注入(DI)容器常常能更优雅地解决"单实例需求"

11. 单例模式的适用场景总结

适合:

  • 需要全局唯一实例的场景
  • 资源初始化成本高且只需要一次
  • 工具类/管理类(配置、日志、缓存等)

不适合:

  • 业务上强依赖可变全局状态(容易造成耦合)
  • 需要频繁创建销毁、区分上下文的对象(这种不应该用单例)

12. 一句话总结:怎么选实现方式?

  • 想简单:饿汉式
  • 想懒加载又要简洁:静态内部类
  • 想懒加载 + 高并发 + 熟悉细节:双重检查锁(DCL)+ volatile
  • 强安全要求(反射/序列化):枚举单例
相关推荐
空空潍2 小时前
Spring AI 实战系列(二):ChatClient封装,告别大模型开发样板代码
java·人工智能·spring
imuliuliang2 小时前
Spring Boot(快速上手)
java·spring boot·后端
va学弟2 小时前
Java 网络通信编程(8):完善 UDP 协议
java·开发语言·udp
夫礼者2 小时前
【极简监控】打破中间件黑盒:用 Micrometer 打造“SLF4J式”的降维打击Metrics监控体系
java·中间件·监控·metrics·micrometer
yashuk2 小时前
Spring Boot 3.4 正式发布,结构化日志!
java·spring boot·后端
daidaidaiyu11 小时前
JMS, ActiveMQ 学习一则
java
weixin_7042660511 小时前
SpringBoot全注解开发指南
java·spring boot·mybatis
星如雨グッ!(๑•̀ㅂ•́)و✧11 小时前
Webflux fromXXX对比
java
competes12 小时前
学生需求 交易累计积分,积分兑换奖品
java·大数据·开发语言·人工智能·java-ee