为什么单例模式的mInstance前面要加volatile

探讨了在单例模式中使用volatile关键字的原因,解释了其如何确保线程安全和避免指令重排序,以及为何在双重检查锁定中至关重要。

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

为什么单例模式的mInstance前面要加volatile

有的老铁看到标题后会有疑问,没遇到过,mInstance要加volatile吗?

还有的人的以为是,synchronized不是可以保证原子性和线程安全吗,怎么还要加volatile?

本次要聊的是单例模式的双重检验加锁实现方式,如下:

public class Single {
    private static volatile Single mInstance = null;
    private Single() {
    }
    public static Single getInstance() {
        if (mInstance==null) {
            synchronized (Single.class) {
                if (mInstance==null) {
                    mInstance = new Single();
                }
            }
        }
        return mInstance;
    }
}
复制代码

这个volatile该不该加?

聊聊volatile的特性

  1. 可见性

可见性是指线程对共享变量进行修改的指令对其他线程来说是可见的,反应指令执行的透明度。

对共享变量的修改操作有哪些?读和写

每个线程有自己的本地内存,本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存进行,操作结束后 再同步到堆内存中。从操作开始到结束这段时间,其他线程是不知道的,即操作不可见。当变量被volatile修饰,任何操作都会在内 存中进行,不会产生副本,从而保证了可见性。

  1. 防止指令重排序

指令重排序是指CPU在处理信息的时候会对一些读数据或者写数据的指令做一个合并进行的优化。

private void operation() {
    //第一处
    int a = 0;
    int b = 1;
    int c = 2;
    //第二处
    int sum = a+b+c;
    //第三处
    int d = 3;
}
复制代码

如上第三处的写操作会被优化放到第二处前执行。若对第三处使用volatile修饰可以让CPU把第三处操作放在第二处操作之后再执行。

  1. 无原子性
volatile int e = 0;

private void addforone() {
    e++;
}
复制代码

e++操作包含读取e,执行e+1,和将e+1赋值给e。第一个线程读取了值,然后执行+1操作,最后把值存到主存。 单个线程是没问题的,多线程情况下,第二个线程在第一个线程把值存到主存之前读取了值,然后+1,再把值存 到主存,这时就会有问题,预期结果是原值+2,实际结果却是原值+1。

回到单例模式

第一个线程调用getInstance(),开始执行mInstance = new Single(),初始化Single实例和将对象地址写到mInstance并非原子操作,且这两个阶段 执行顺序不确定。由于java编译器的优化导致对象初始化还没完成,mInstance引用值提前写入。假设第一个线程的new Single() 执行到构造方法之前,构造方法还没调用,编译器只是为该对象分配了空间并设置默认值。若此时第二个线程调用getInstance(), 由于mInstance!=null,但是mInstance指向的对象未被赋予正确的值,第二个线程获取到的不是正确的单例对象。

结语

为了避免获取不到正确的单例对象,要用volatile修饰单例对象。

转载于:https://juejin.im/post/5caff131f265da0374186a4d

Java中实现单例模式时,使用**双重检查锁定**(Double-Checked Locking,DCL)是为了在保证线程安全的同时,提升性能。传统的单例实现方式(如直接在`getInstance()`方法上使用`synchronized`关键字)会导致每次调用该方法时都需要获取锁,这在多线程环境下会对性能造成较大影响。而双重检查锁定通过减少锁的持有时间,优化了这一过程。 双重检查锁定的核心思想是在进入同步代码块之前先进行一次检查,只有在单例实例尚未创建时才进入同步块。进入同步块后,再进行一次检查,以确保没有其他线程在同步块外检查为`null`之后创建了实例。这种双重检查机制确保了单例的唯一性,同时避免了不必要的同步开销。 此外,为了确保实例的创建过程在多线程环境下的可见性和有序性,需要将单例实例声明为`volatile`类型。`volatile`关键字的作用包括: - **保证可见性**:一个线程对`volatile`变量的修改对其他线程是立即可见的。 - **防止指令重排序**:在创建对象时,JVM可能会对指令进行重排序以优化性能,这可能导致对象未完全构造完成就被其他线程访问。使用`volatile`可以避免这种情况,确保对象在构造完成之后才被赋值给引用变量[^2]。 以下是一个典型的双重检查锁定实现单例的示例代码: ```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(); // 创建实例 } } } return instance; } } ``` 代码中,第一次检查`instance == null`是为了避免不必要的同步,第二次检查是为了确保只有一个线程能够创建实例。这种方式在保证线程安全的前提下,有效减少了锁的使用频率,从而提高了性能[^4]。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值