单例模式singleton为什么要加volatile

本文探讨了在Java中使用双重检查加锁(double-checked locking)模式时,volatile关键字的作用及重要性。通过示例说明了仅依靠synchronized无法完全避免多线程环境下对象初始化时可能出现的问题,并解释了如何利用volatile确保指令正确执行顺序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

或者许多的朋友在使用单例模式中会学到双重检查加锁(double-checked locking)的应用。

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(uniqueInstance == null){
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

但同时也会产生一个问题:为什么要用到volatile。这里不妨先看看一牛人对于volatile关键字的解读。 
作者:海子 
     
出处:http://www.cnblogs.com/dolphin0520/p/3920373.html 
     
看完volatile之后,你就可以对于指令重排序有进一步的了解后,尤其是volatile可以保证即使java虚拟机对代码执行了指令重排序,也会保证它的正确性。 
然后我们来指出下为什么用了synchronized还要用volatile,以及可能出现的指令重排序影响双重检查加锁(double-checked locking)的正确性。 
可以看以下例子。

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(uniqueInstance == null){
        // B线程检测到uniqueInstance不为空
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                    // A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
                }
            }
        }
        return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。
    }
}

具体来说就是synchronized虽然保证了原子性,但却没有保证指令重排序的正确性,会出现A线程执行初始化,但可能因为构造函数里面的操作太多了,所以A线程的uniqueInstance实例还没有造出来,但已经被赋值了。而B线程这时过来了,错以为uniqueInstance已经被实例化出来,一用才发现uniqueInstance尚未被初始化。要知道我们的线程虽然可以保证原子性,但程序可能是在多核CPU上执行。

<think>好的,我现在要回答用户关于单例模式中双重检查锁为何使用volatile关键字的问题。首先,我需要回忆单例模式的基本概念,尤其是双重检查锁的实现方式。 用户提到的双重检查锁(Double-Checked Locking)通常用于多线程环境下确保单例实例的唯一性,同时减少同步开销。我记得这种模式会先检查实例是否已创建,如果未创建才进入同步块,再次检查后再创建实例。这样做的好处是只有第一次创建实例时会同步,后续调用直接返回实例,提高性能。 接下来,volatile关键字的作用是关键。根据Java内存模型,变量的读写可能会被缓存到线程本地内存,导致可见性问题。如果没有volatile,其他线程可能看到未完全初始化的实例。例如,线程A在创建实例时,可能在构造函数未执行完时,实例的引用就被写回主内存,线程B拿到的是一个不完整的对象,导致程序出错。 然后,我需要参考用户提供的引用内容。引用[4]提到,当线程1执行构造方法但未完成赋值时,线程2可能会获取到null的实例,进入同步块等待。而如果使用了volatile,就能保证写操作的可见性和有序性,防止指令重排序。这里的关键在于volatile禁止了JVM和处理器对指令的重排序,确保实例的初始化过程按顺序完成,避免其他线程看到半初始化的状态。 此外,引用[1]和[2]都提到volatile在双重检查锁中的必要性,主要是为了延迟初始化变量并保证线程安全。而引用[3]虽然讨论的是C++,但原理类似,强调了内存屏障的作用。 总结起来,volatile在双重检查锁中的作用主要有两点:保证可见性,即一个线程对变量的修改对其他线程立即可见;禁止指令重排序,确保对象初始化完成后再被引用。没有volatile的话,可能会因为指令重排序导致其他线程获取到未初始化的实例,从而引发错误。 最后,我需要将以上思考整理成结构化的回答,分点解释原理,并引用相关资料作为支持。同时,生成相关问题,帮助用户进一步理解该主题。</think>### 单例模式中双重检查锁与 volatile 的作用原理 #### 一、双重检查锁的基本结构 双重检查锁定(Double-Checked Locking)是一种在多线程环境下**延迟初始化单例对象**的优化模式。其典型实现如下(以Java为例): ```java public class Singleton { private static volatile Singleton instance; // 关键点:volatile修饰 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { // 同步锁 if (instance == null) { // 第二次检查 instance = new Singleton(); // 实例化 } } } return instance; } } ``` #### 二、volatile 关键字的必要性 ##### 1. **禁止指令重排序** 对象初始化过程 `instance = new Singleton()` 包含三个步骤: 1. 分配内存空间 2. 调用构造函数初始化对象 3. 将内存地址赋值给 `instance` 变量 若**没有** `volatile`,JVM可能对指令进行重排序(步骤2和3交换顺序)。此时其他线程可能在第一次检查时看到 `instance` 不为 `null`,但实际对象尚未完成初始化,导致使用**半初始化状态的对象**[^4]。 ##### 2. **保证可见性** `volatile` 确保一个线程对变量的修改能**立即对其他线程可见**。若缺少此修饰,线程可能从本地缓存中读取旧值,导致多次实例化或空指针异常[^2]。 #### 三、原理示意图 ``` 线程A: 执行 instance = new Singleton() ┌── 分配内存 → 内存地址赋值 → 初始化对象 (无volatile时可能重排序) └── 分配内存 → 初始化对象 → 内存地址赋值 (有volatile时禁止重排序) 线程B: 检测到 instance 不为 null → 直接使用(可能拿到未初始化的对象) ``` #### 四、双重检查锁的优化意义 - **减少锁竞争**:仅在首次初始化时使用同步锁 - **提升性能**:避免每次调用 `getInstance()` 都
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值