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未进行初始化的问题了
  
  
  

 —参考文献 《 深入理解Java虚拟机 》 
      《Java并发编程的艺术》 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值