1. 一个单例引发的线上事故:从根源说起
先说个我实际经历过的线上问题。当时一个订单服务在压测时偶发出现"订单号重复"的诡异日志,排查了大半天,最后发现罪魁祸首竟然是——一个单例对象被创建了多份。一个订单号生成器,设计时用单例模式确保全局唯一,结果在某些并发条件下,不同线程拿到的不是同一个实例,各自维护了一份独立的序号池,订单号自然就重复了。
这不是段子,是真实事故。很多人觉得单例模式简单到不值一提——"不就是构造函数私有化加一个静态方法吗?"——但单例恰恰是最容易在多线程环境下写崩的代码模式之一。不管是Java、C++还是C#,只要涉及多线程,单例的线程安全问题就会从"基础题"变成"送命题"。最简单的if (instance == null)判断,放在并发环境下,随时可能给你搞出几个"单例"来。
这篇博文就围绕"多线程 + 单例"这个组合做一次完整的拆解:从问题根源,到各种写法演进,再到面试追问,最后给出我踩坑多年的选型建议。无论你是刚接触多线程的新手,还是写了几年业务代码的老手,我相信总有你能用上的东西。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么最简单的懒汉式在多线程下必挂
先看最经典的懒汉式单例写法,几乎所有教材里都出现过:
java复制public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造
}
public static Singleton getInstance() {
if (instance == null) { // 线程A、B同时到达这里
instance = new Singleton(); // 然后各自创建实例
}
return instance;
}
}
这段代码单线程下毫无问题,但放到多线程环境里,它的正确性完全建立在运气上。
2.1 竞态条件:两个线程同时看到了"空"
问题出在if (instance == null)这个判断不是原子操作,new Singleton()也不是原子操作。当两个线程同时调用getInstance()时,可能发生这样的情况:
- 线程A先执行
if (instance == null),发现instance确实是null,然后进入if代码块,准备执行new。 - 就在A还没完成
new Singleton()赋值时,线程B也执行了if (instance == null),它看到的instance依然是null,于是也进入了if代码块。
结果:A和B各自new了一个Singleton对象,全局唯一的约束被打破。更细的问题,扒到CPU和内存层面更值得聊——可见性和重排序才是这里真正的主角。
2.2 加了同步方法就安全了吗
很自然的想法是给getInstance()加synchronized:
java复制public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全确实解决了,但性能代价非常大。注意,synchronized锁的是整个方法——当实例已经创建完毕,后续所有的getInstance()调用依然要串行拿锁。在高并发场景下,所有线程相当于排着队过一个单行道闸机,吞吐量直线下降。
我见过不少项目早期这样写,访问量小的时候没什么感觉,一上压测就原形毕露。打个不恰当的比方:就像一个已经开门营业的商店,每来一个顾客你都要临时关门确认"店里有没有人",确认完了再放他进来,这不是搞笑吗?
这里其实引出了后面很多优化的核心逻辑:锁的粒度要尽量小,而且只在真正需要同步的阶段才同步。实例已经创建之后,根本不需要再走锁。
3. 双重检查锁定(DCL)与volatile:一次被重排序坑惨的优化
为了解决"每次调用都加锁"的浪费,聪明的前辈们设计出了双重检查锁定(Double-Checked Locking,简称DCL),这是面试必考,也是实际项目里用得最多的一种方案。
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();
}
