单例模式真的线程安全吗?揭秘 volatile + synchronized 如何联手打造高性能单例!

面试官:你知道单例模式吗?如果多线程环境下怎么保证线程安全?

很多人回答:

“加 synchronized 就行。”

但是面试官继续追问:

“为什么还需要 volatile?”

“不用 volatile 会有什么问题?”

如果答不上来,说明你只是会写代码,并没有理解 JVM 底层原理。

今天我们深入理解 volatile 和 synchronized 如何实现双重检查锁单例模式(Double Check Lock,DCL)


一、什么是单例模式?

单例模式(Singleton Pattern):

保证一个类在整个应用中只创建一个实例,并提供一个全局访问入口。

例如:

Singleton s1 = Singleton.getInstance();

Singleton s2 = Singleton.getInstance();

希望:

s1
 |
 ↓

同一个对象

 ↑
 |
s2

两个变量指向同一个对象。


二、最简单的单例写法有什么问题?

很多初学者会这样写:

public class Singleton {

    private static Singleton instance;


    private Singleton(){}


    public static Singleton getInstance(){

        if(instance == null){

            instance = new Singleton();

        }

        return instance;
    }
}

看起来没问题。

但是在多线程环境:

假设两个线程同时调用:

线程A                     线程B

getInstance()             getInstance()

判断 instance == null     判断 instance == null


创建对象                  创建对象


返回对象A                 返回对象B

结果:

出现两个对象

单例失败。


三、使用 synchronized 解决线程安全问题

我们给创建对象部分加锁:

public static synchronized Singleton getInstance(){

    if(instance == null){

        instance = new Singleton();

    }

    return instance;
}

这样:

线程A进入方法

        ↓

获取锁

        ↓

创建对象

        ↓

释放锁


线程B才能进入

保证:

同一时间只有一个线程创建对象。


但是新的问题出现:

synchronized 性能问题

每次调用:

Singleton.getInstance();

都需要:

获取锁
 ↓
检查
 ↓
返回
 ↓
释放锁

可是:

对象创建一次之后:

instance != null

后面的线程根本不需要加锁。

所以性能不好。


四、什么是双重检查锁(DCL)?

于是出现:

Double Check Lock(双重检查锁)

代码:

public class Singleton {


    private volatile static Singleton instance;


    private Singleton(){}



    public static Singleton getInstance(){


        // 第一次检查
        if(instance == null){


            synchronized(Singleton.class){


                // 第二次检查
                if(instance == null){

                    instance = new Singleton();

                }

            }

        }


        return instance;
    }

}

这个就是 Java 中经典的高性能单例实现。


五、为什么需要两次 if 判断?

第一次判断

if(instance == null)

作用:

减少加锁次数,提高性能。

如果对象已经存在:

instance != null

直接返回。

不用进入:

synchronized

第二次判断

进入锁之后:

synchronized(Singleton.class){

    if(instance == null){

        instance = new Singleton();

    }

}

作用:

防止多个线程同时创建对象。

例如:

线程A:

进入锁

创建对象

线程B:

等待锁

当线程B获得锁:

第一次判断已经通过。

但是需要再次判断:

if(instance == null)

发现:

instance已经存在

不会重复创建。


六、重点来了:为什么还需要 volatile?

很多人认为:

有 synchronized 就够了,为什么还需要 volatile?

这是面试高频问题。

原因:

对象创建不是一步完成

代码:

instance = new Singleton();

看起来是一行。

实际上 JVM 大概分三步:

第一步

分配内存:

给对象分配空间

第二步

初始化对象:

执行构造方法

第三步

让 instance 指向对象:

instance = 内存地址

正常:

分配内存

↓

初始化对象

↓

instance指向对象

但是 JVM 为了优化,可能发生指令重排序:

变成:

分配内存

↓

instance指向对象

↓

初始化对象

问题来了:

线程A:

执行:

instance = new Singleton();

但是对象还没有初始化完成。

线程B:

执行:

if(instance != null)

发现:

instance不是null

直接返回。

但是拿到的是:

一个还没有初始化完成的对象。

可能导致程序异常。


七、volatile 解决什么问题?

volatile:

private volatile static Singleton instance;

解决两个问题:

1. 禁止指令重排序

保证:

分配内存

↓

初始化对象

↓

引用赋值

不会乱序。


2. 保证线程可见性

例如:

线程A修改:

instance = new Singleton();

其他线程能够立即看到。


所以:

volatile

防止拿到半初始化对象

八、volatile 和 synchronized 分别负责什么?

一句话记忆:

关键字作用
synchronized保证同一时间只有一个线程创建对象
volatile保证对象创建过程正确,并保证变量可见性

简单理解:

synchronized

防止创建多个对象


volatile

防止拿到错误对象

九、完整生产级代码

public class Singleton {


    private volatile static Singleton instance;


    private Singleton(){}



    public static Singleton getInstance(){


        if(instance == null){


            synchronized(Singleton.class){


                if(instance == null){

                    instance = new Singleton();

                }

            }

        }


        return instance;

    }

}

十、面试最终回答(30秒版本)

如果面试官问:

volatile 和 synchronized 如何实现单例模式?

可以回答:

通常使用双重检查锁 DCL 实现。synchronized 保证对象创建过程只有一个线程执行,避免多线程情况下创建多个实例;volatile 用来禁止 JVM 指令重排序,并保证 instance 对其他线程立即可见,防止其他线程获取到未初始化完成的对象。两者结合实现线程安全且性能较高的单例模式。


总结

单例模式真正难的不是代码:

private volatile static Singleton instance;

而是理解背后的 JVM 原理:

synchronized
        ↓
保证线程安全

volatile
        ↓
保证内存可见性 + 防止指令重排序

两者结合
        ↓
高性能线程安全单例

这也是 Java 后端面试中非常经典的并发问题。掌握它,不仅能回答单例模式,还能体现你对 JVM、并发编程、Java 内存模型(JMM) 的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值