volatile关键字在单例模式中的应用

      这几天在研究volatile关键字,有看书,上网找博客,本来看的还挺好的理解的,但是卡在了一个地方,就是单例模式中懒汉模式使用Double Check里面的volatile的作用原理弄糊涂了。不同地方有不同的说法,最后终于理清了。

     关键字volatile可以说是Java虚拟机提供的最轻量的同步机制,但是它并不容易完全被正确、完整地理解,以至于许多程序员都不习惯去使用,遇到需要处理多线程数据竞争问题的时候一律使用synchronized来进行同步。但是两者各有各的优点,我们有必要去了解清楚volatile这个关键字。


两个特性

当一个变量定义为volatile之后,它将具备两种特性 
1. 保证此变量对所有线程的可见性 
2. 禁止指令重排序优化 

è¿éåå¾çæè¿°

保证此变量对所有线程的可见性
  这里不做过多解释,简单的说就是,当一个线程修改了volatile变量之后,它先写入它的工作内存中,然后立刻写入主内存,并且刷新其他线程中的工作内存,这样其他线程再去读取他们工作内存中的变量时,确保能够拿到最新的。但是如果是普通变量的话,它不会立即写入主内存中,所有其他线程的工作内存中保存的是旧的值。所有volatile变量可以保证可见性。 


禁止指令重排序优化

这个是保证单例模式不出错的原因,我们先来看看DCL单例模式(Double Check)

先看看最简单的单例模式

//Version 1

public class Single1 {
    private static Single1 instance;
    private Single1() {}
    public static Single1 getInstance() {
        if (instance == null) {
            instance = new Single1();
        }
        return instance;
    }
}

这样做问题出在,多线程调用getInstance,多线程都读到instance是null,这样多线程创建了多个实例,明显会有问题。

如果我们在方法上加锁

// Version 2 

public class Single2 {
    private static Single2 instance;
    private Single2() {}
    public static synchronized Single2 getInstance() {
        if (instance == null) {
            instance = new Single2();
        }
        return instance;
    }
}

这样可以保证每次只有一个线程进入方法,看起来好像没什么问题。但是这种方法会带来效率问题,一个线程在调用这个方法时,不论instance是不是null,其他线程调用这个方法一定处在等待状态,这个是很浪费资源的。

于是人们想到了Double Check这种聪明的方法。

// Version 3 
public class Single3 {
    private static Single3 instance;
    private Single3() {}
    public static Single3 getInstance() {
        if (instance == null) {
            synchronized (Single3.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}

 这个方法解决了效率问题,当instance不为null时不用等待。看起来好像没什么问题了,但是instance = new Single3()不是一个原子操作,它可以分成三个指令操作。 
1. 给 singleton 分配内存 
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例 
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

      而计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。就是说,这三个操作可以是1-2-3,也可以是1-3-2。如果在1-3做完之后,instance已经不为null了。但是它还没有初始化这个实例,那么其他线程这个时候执行返回的instance就是错误的了。 
 

但是volatile可以解决这个问题。

// Version 4 

public class Single4 {
    private static volatile Single4 instance;
    private Single4() {}

    public static Single4 getInstance() {
        if (instance == null) {
            synchronized (Single4.class) {
                if (instance == null) {
                    instance = new Single4();
                }
            }
        }
        return instance;
    }
}

      加上关键字之后,禁止了指令重排优化,结果就是 指令一定会按照1-2-3进行,然后再到判断instance是否为null,那么这个禁止指令重排优化的规则是怎样的呢?

1.当后一个操作是volatile写时,不管前一个操作是什么(volatile读/写,普通变量读/写),都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
2.当前一个操作是volatile读时, 不管后一个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
3.当前一个操作是volatile写、后一个是volatile读时,不能重排序。


所以上面单例模式是运用到了第1条规则,不允许1-2排到3后面。这样就不会读到instance不为null,但是instance未进行初始化的问题了 
 

 

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值