【自存】懒汉式单例模式中的多线程经典疑问

单例模式

单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。这一点在很多场景上都需要。比如 JDBC 中的 DataSource 实例就只需要一个。单例模式的具体实现方法又分为饿汉懒汉两种:

  • 饿汉:指的是在创建一个类的时候就将实例创建好!比较急!
  • 懒汉:指的是在需要用到实例的时候再去创建实例!比较懒!

因为我们单例模式只能有一个实例,那如何去保证一个实例呢?我们会马上想到类中用 static 修饰的类属性,它只有一份!保证了单例模式的基本条件!

情况一:饿汉

class Singleton
{
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance() {
return instance;
}
}

我们可以看到这里饿汉模式,当多个线程并发时,并不会出现线程不安全问题,因为这里的设计模式只是针对了读操作!而单例模式的更改操作,需要看懒汉模式!

情况二:懒汉

实际情况是,只有当首次调用单例时在进行创建:

class Singleton1
{
private static Singleton1 instance = null;
private Singleton1(){
}
public static Singleton1 getInstance() {
if(instance==null){
//①:需要时再创建实例!
instance = new Singleton1();
//②
}
return instance;
}
}

❗ 存在的问题:可能创建多个实例!
假设两个线程 A 和 B 同时调用 getInstance()

  • 两者都执行到 ①,发现 instance == null → 都进入 if 块。
  • 两者都执行 ② → 各自 new 了一个对象!
  • ❌ 违反了“单例”的核心原则:全局唯一实例。

情况三:简单加锁

针对上面的情况,一个很直观的思路是:给读写操作进行加锁。所有线程在进入这个 synchronized (Singleton.class) 代码块前,必须先获取这个 Class 对象的内置锁(monitor)。同一时刻,只有一个线程能持有这个锁,其他线程必须等待。

class Singleton2
{
private static Singleton2 instance = null;
private Singleton2(){
}
public static Singleton2 getInstance() {
synchronized (Singleton.class)
{
//对读写操作进行加锁!
if(instance==null){
//需要时再创建实例!
instance = new Singleton2();
}
return instance;
}
}
}

等价于

public static synchronized Singleton2 getInstance() {
if(instance==null){
//需要时再创建实例!
instance = new Singleton2();
}
return instance;
}

❗ 仍然存在的问题:所有调用都要加锁!

  • 即使 instance 已经初始化完成,每次读取调用 getInstance() 仍要竞争锁。
  • 单例对象创建后是“只读”的,后续读取完全没必要加锁!
  • ❌ 高并发场景下成为性能瓶颈。

情况四:双重检查锁

针对上面问题,引入双重检查锁:双重检查锁(DCL)的精髓在于:

  • 第一次检查(无锁) → 如果对象已创建,直接返回,完全不加锁,性能极高!
  • 只有在对象未创建时,才加锁 + 第二次检查。
class Singleton2
{
private static Singleton2 instance = null;
private Singleton2(){
}
public static Singleton2 getInstance() {
if(instance==null){
//如果未初始化就进行加锁操作!
synchronized (Singleton.class)
{
//对读写操作进行加锁!
if(instance==null){
//需要时再创建实例!
instance = new Singleton2();
}
}
}
return instance;
}
}

❗ 但是!这段代码仍然有问题:
⚠️ 核心问题:JVM 的指令重排序导致其他线程可能拿到未初始化的对象!
如前所述,instance = new Singleton2(); 在底层分为三步:

  1. 分配内存
  2. 初始化对象(构造方法)
  3. instance 引用指向内存地址

JVM 可能重排序为:1 → 3 → 2
线程 A 执行到第 3 步(instance != null),但对象还没初始化(第 2 步没执行)
线程 B 跳过第一个 if(因为 instance != null),直接 return instance
线程 B 拿到的是半初始化对象 → 调用方法或访问字段时可能崩溃!

问题:第二if是否可以删掉?

答:不可以,我们发现当有多个线程进行了第一个 if 判断后,进入的线程中有一个线程锁竞争拿到了锁,而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,如果没有 if 判断,新拿到锁的线程又会执行实例创建代码,这样也就多次创建了实例,显然不可!!!

情况五:终极解决方案

加上 volatile 关键字, volatile 的作用:

  • 禁止指令重排序 → 保证“初始化完成”再赋值给 instance。
  • 保证可见性 → 一个线程修改 instance,其他线程立即可见。
class Singleton2
{
private static volatile Singleton2 instance = null;
private Singleton2(){
}
public static Singleton2 getInstance() {
if(instance==null){
//如果未初始化就进行加锁操作!
synchronized (Singleton.class)
{
//对读写操作进行加锁!
if(instance==null){
instance = new Singleton2();
}
}
}
return instance;
}
}
posted @ 2025-09-17 14:54  yxysuanfa  阅读(17)  评论(0)    收藏  举报