Java设计模式-单例模式

概述

单例模式(Singleton Pattern)是 Java 中最常用、最基础的创建型设计模式之一。

它的核心目标是:确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。

适用于:

1、需要频繁创建和销毁的对象;

2、创建消耗资源太多有会频繁创建的对象;

3、有状态的工具类对象;

4、频繁访问数据库或文件的对象。

在 Java 中,这意味着:

该类不能被外部随意 new

整个 JVM 中只存在一个该类的对象;

所有需要使用该对象的地方都通过同一个入口获取。

典型场景

场景 说明
配置管理器 应用启动时加载一次配置,后续所有模块共享同一份配置
日志记录器 避免多个 Logger 实例导致日志混乱或文件锁冲突
数据库连接池 连接池本身应是全局唯一的资源管理中心
缓存中心 如内存缓存(如本地 Map 缓存),需统一管理
计数器/ID 生成器 需保证全局唯一性

核心要素

私有构造方法:防止外部通过 new 创建实例。

持有自身静态实例:通常用 private static 修饰。

提供公共静态方法获取实例:如 getInstance()。

常见实现方式

1. 饿汉式(Eager Initialization)
java 复制代码
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

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

优点:线程安全、实现简单

缺点:类加载时就创建实例,可能浪费资源(即使没用到)

2. 懒汉式(Lazy Initialization)--- 非线程安全
java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

多线程下可能创建多个实例,不推荐使用。

3. 懒汉式 + 同步方法(线程安全但低效)
java 复制代码
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

线程安全

每次调用都要同步,性能差

4. 双重检查锁定(Double-Checked Locking) 推荐
java 复制代码
public class Singleton {
    // volatile 防止指令重排序
    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;
    }
}

线程安全、延迟加载、高性能,适用于高并发环境

注意:必须加 volatile,防止 JVM 指令重排导致未初始化完成的对象被引用。

关键点:volatile 的作用

instance = new DCLSingleton(); 实际分三步:

  1. 分配内存空间
  2. 调用构造方法,初始化对象
  3. instance 指向分配的内存地址

JVM 可能重排序为:1 → 3 → 2(指令重排优化)

如果没有 volatile

  • 线程 A 执行到第 3 步(instance != null),但对象未初始化完成
  • 线程 B 调用 getInstance(),看到 instance != null,直接返回
  • B 使用了一个未完全构造的对象空指针或状态错误!

volatile 禁止指令重排序 + 保证可见性

5. 静态内部类(推荐)
java 复制代码
public class Singleton {
    private Singleton() {}

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

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

线程安全(由 JVM 类加载机制保证)

延迟加载(只有调用 getInstance() 时才加载内部类)

代码简洁、无锁、性能高

强烈推荐用于大多数场景

6. 枚举方式(最安全)
java 复制代码
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // 业务方法
    }
}

天然线程安全

防止反射攻击

防止反序列化破坏单例(枚举的序列化机制特殊)

《Effective Java》作者 Joshua Bloch 强烈推荐

使用方式:Singleton.INSTANCE.doSomething();

为什么最安全
攻击方式 普通单例 枚举单例
反射 可通过 setAccessible(true) 调用私有构造器 枚举构造器不能被反射调用(JVM 层面禁止)
反序列化 若未实现 readResolve(),会创建新对象 枚举的序列化机制特殊,反序列化返回原实例
多实例 可能因上述原因被破坏 绝对保证单例

缺点:无法继承(枚举隐式继承 Enum);不能懒加载(其实枚举实例也是类加载时初始化,类似饿汉式);部分框架(如某些老 ORM)对枚举支持不好。

7、性能对比

实现方式 线程安全 延迟加载 性能 安全性
饿汉式
懒汉式(无锁)
synchronized 方法
DCL(volatile) 高(需正确实现)
静态内部类
枚举 ❌(类加载时) 最高

📌 实际性能差异在现代 JVM 上极小,安全性 > 微优化

注意事项

问题 说明
反射攻击 通过反射可调用私有构造器,破坏单例(枚举可防御)
序列化/反序列化 反序列化可能创建新对象,需实现 readResolve() 方法: private Object readResolve() { return INSTANCE; }
多类加载器 不同 ClassLoader 可能加载多个单例(一般应用无需考虑)

常见误区

误区1:单例能解决所有"全局状态"问题

→ 单例本质是全局状态,过度使用会导致:

  • 代码耦合度高
  • 难以单元测试(需 mock 单例)
  • 隐藏依赖(调用方不显式传参)

替代方案:考虑依赖注入(DI),如 Spring 的 @Component + 单例作用域

误区2:单例一定是"好"的

→ 如果对象状态会变,且被多线程修改,仍需同步控制(单例 ≠ 线程安全对象)

误区3:Spring 中的 Bean 默认是单例,所以不用自己写

→ 正确!在 Spring 应用中,通常用 @Service / @Component 即可,容器管理生命周期。

如何选择

你的需求 推荐实现
追求极致安全、不怕饿汉式 枚举
需要延迟加载、代码清晰 静态内部类
老项目、必须兼容 JDK 1.4 DCL(但尽量升级)
Spring 项目 直接用 @Component,别手写单例
相关推荐
老骥伏枥~2 小时前
VB.NET 中的单例模式
单例模式·.net
小手cool2 小时前
在保持数组中对应元素(包括负数和正数)各自组内顺序不变的情况下,交换数组中对应的负数和正数元素
java
笨手笨脚の2 小时前
深入理解 Java 虚拟机-04 垃圾收集器
java·jvm·垃圾收集器·垃圾回收
skywalker_112 小时前
Java中异常
java·开发语言·异常
没有天赋那就反复2 小时前
JAVA 静态方法
java·开发语言
Java天梯之路3 小时前
Spring Boot 钩子全集实战(七):BeanFactoryPostProcessor详解
java·spring boot·后端
wr2005143 小时前
第二次作业,渗透
java·后端·spring
阿蒙Amon3 小时前
C#每日面试题-Thread.Sleep和Task.Delay的区别
java·数据库·c#
Haooog3 小时前
AI应用代码生成平台
java·学习·大模型·langchain4j