面试官:你知道单例模式吗?如果多线程环境下怎么保证线程安全?
很多人回答:
“加 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) 的理解。

209

被折叠的 条评论
为什么被折叠?



