并发编程(二十二):单例模式:从基础实现到 Spring Web 实战

单例模式是Java开发中最常用、面试最高频的设计模式之一,它看似简单,实则蕴含深厚的并发编程底层原理,并且在Spring、Servlet等主流框架中有着广泛的落地应用。

一、单例模式与设计原则

1 单例模式:

一个类在 JVM 运行期间,有且仅有一个实例对象

  • 生活类比:世界上只有一个中国,无论何人询问,指向的都是同一个主体
  • 程序场景:配置管理器、数据库连接池、线程池、日志工具类,全局仅需一个实例即可支撑全流程操作

2 单例模式两大要求

  1. 私有构造方法
  2. 公共静态获取实例方法

二、单例模式实现方案示例

1 饿汉式单例(简单、线程安全)

类加载时立即初始化实例,心急如饿汉,无需等待使用时创建

java 复制代码
class HungrySingleton {
    // 类加载时创建实例,final保证引用不可二次赋值
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    // 私有构造,禁止外部实例化
    private HungrySingleton() {}

    // 公共获取方法
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}
优缺点分析

✅ 优点:实现极简,依靠 JVM 类加载的线程安全性,天然无并发问题

❌ 缺点:非懒加载,类加载即创建实例,若实例长期未使用,会造成内存浪费

2 双重校验锁(DCL)懒汉式

懒汉式核心:懒加载,首次使用时才创建实例,节约内存资源 。DCL(Double Check Lock)是懒汉式的最优实现,融合双重判空 + 类锁 + volatile,兼顾线程安全与高性能。

java 复制代码
class DclSingleton {
    // volatile:禁止指令重排序,解决半初始化对象问题
    private volatile static DclSingleton INSTANCE;

    private DclSingleton() {}

    public static DclSingleton getInstance() {
        // 第一层判空:避免每次获取都加锁,大幅提升性能
        if (INSTANCE == null) {
            // 类锁:保证多线程下实例创建的原子性
            synchronized (DclSingleton.class) {
                // 第二层判空:防止多线程同时突破第一层判空后重复创建
                if (INSTANCE == null) {
                    INSTANCE = new DclSingleton();
                }
            }
        }
        return INSTANCE;
    }
}
volatile 的必要性(复习)

new DclSingleton()在 JVM 中分三步执行:

  1. 分配对象内存空间
  2. 初始化对象属性
  3. INSTANCE引用指向内存空间

JVM 可能对指令重排序,执行顺序变为1→3→2。若线程 A 执行完步骤 3,对象尚未初始化,线程 B 判断INSTANCE!=null,会获取到半初始化的异常对象

volatile的作用:禁止指令重排序,保证实例完全初始化后,其他线程才能读取到引用。

三、单例模式与并发关键字关联

知识点 在单例模式中的作用
final(饿汉式) 修饰实例引用,保证INSTANCE指向唯一对象,禁止二次赋值
volatile(DCL) 禁止new指令重排序,杜绝半初始化对象问题
指令重排序 DCL 必须加 volatile 的核心原因,与 final 内存语义底层逻辑一致

四、单例模式在 Servlet/Spring Controller 中的应用

1 场景

Servlet、Spring MVC 的Controller默认采用单例作用域,是单例模式在 Web 开发中经典的落地场景。

2 为什么 Servlet/Controller 要设计为单例?

方法调用底层原理 非静态方法必须依托对象调用,但线程调用方法时,会将方法栈帧拷贝到自身线程栈独立执行,不同线程的栈帧互不干扰,方法层面天然线程安全。

单例的内存价值

反例:1 万个请求→创建 1 万个 Servlet 对象→频繁创建销毁,加重 GC 压力,严重浪费内存

正例:全局仅 1 个对象→所有线程共享对象,拷贝方法执行→内存利用率最大化

3 内存可视化示例

4 单例安全的前提

Servlet/Controller 单例安全的关键是无状态设计

  • 类中不定义实例变量、共享静态变量,仅做请求处理、逻辑转发
  • 若定义共享成员变量,多线程并发修改会引发线程安全问题

Servlet、Controller 这类无状态的请求处理类,采用单例模式,全局仅创建一个对象,所有请求线程共享该对象并独立拷贝方法栈帧执行,既满足非静态方法的调用要求,又保证线程安全,同时大幅节省内存与 GC 资源。

五、枚举单例深度剖析(最安全但有坑)

1 枚举单例实现(代码极简)

枚举是《Effective Java》推荐的单例实现,JVM 底层天然保证单例

java 复制代码
// 枚举单例:JVM保证唯一实例
enum ServletSingleton {
    // 全局唯一的枚举实例
    INSTANCE;

    // 业务方法(对应Servlet的doGet/doPost)
    public void doGet() {
        System.out.println("处理GET请求");
    }

    public void doPost() {
        System.out.println("处理POST请求");
    }
}

2 枚举单例的优势

1 JVM 天然线程安全:枚举实例在类加载时由 JVM 唯一创建,无并发问题

2 防反射、防序列化破坏:普通单例可通过反射调用私有构造、反序列化创建新对象,枚举从底层杜绝该漏洞

3 代码简单:无需手动处理锁、volatile,开发成本低

3 枚举单例的缺点

饿汉式加载,无法懒加载:类加载时即创建实例,无论是否使用;

启动性能损耗:若项目中有大量枚举单例,启动时会一次性创建所有实例,拖慢启动速度,浪费内存资源。

4 枚举单例 VS DCL 单例

特性 枚举单例 DCL 单例
实现难度 极简 稍复杂(双判空 + volatile + 类锁)
线程安全 JVM 天然保证 手动编码保证
加载时机 类加载时(饿汉) 首次调用时(懒汉)
内存占用 启动时提前占用 按需创建,更节省
防反射 / 序列化 天然支持 需额外编码处理
适用场景 简单、内存不敏感场景 高并发、内存敏感的企业级 Web 项目

枚举单例是 JVM 层面最安全的单例实现,天然防反射、防序列化破坏,代码简洁,但属于饿汉式加载无法懒加载,适合轻量场景

DCL 懒汉式支持懒加载、性能优异,更适合高并发、内存敏感的生产级 Web 项目

六、要点总结

  1. 单例本质 :一个类全局仅有一个实例,依靠私有构造 + 公共静态获取方法实现;、
  2. 经典实现选型 :饿汉式简单安全但非懒加载,DCL 懒汉式是面试最优解,volatile是核心
  3. 底层关联:volatile 禁止指令重排序,解决半初始化对象问题,与 final 内存语义同源
  4. Web 实战:Servlet/Controller 默认单例,无状态设计保证线程安全,是内存优化的关键
  5. 枚举单例:最安全但无懒加载,与 DCL 单例场景互补,需根据业务按需选择
相关推荐
小王不爱笑1322 小时前
Java Map 三大核心实现类详解:HashMap、TreeMap、Hashtable
java·开发语言·哈希算法
1104.北光c°2 小时前
双令牌机制:让认证更安全、体验更流畅
java·开发语言·笔记·后端·安全·token·双令牌
独自破碎E2 小时前
【面试真题拆解】Java文件操作的异常类型与受检_非受检异常
java·面试·职场和发展
q5431470872 小时前
Spring TransactionTemplate 深入解析与高级用法
java·数据库·spring
工一木子2 小时前
String.format 替换踩坑记:从遇坑、读源码到手写实现
java·jdk源码
6+h2 小时前
【java IO】IO体系结构 + File类详解
java·数据库·php
海南java第二人2 小时前
Flink状态后端与容错机制深度剖析:TB级状态下的高可用实战
java·spring·flink
不光头强2 小时前
Java网络爬虫
java·爬虫·python