DCL下的happens-before问题研究

看到一篇博客上讲happens-before

其中举了一个例子,就是使用DCL的单例模式的写法,可能会产生其他线程读到未能被完全初始化的对象的问题。

也就是下面的代码,如果线程1执行getInstance(),而线程2执行getInstance()和getSomeField()的话, someField的值可能会是0。

据说这个例子出自某本书,不过我没看过。

public class LazySingleton {
    private int someField;
    
    private static LazySingleton instance;
    
    private LazySingleton() {
        this.someField = new Random().nextInt(200)+1;         // (1)
    }
    
    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }
    
    public int getSomeField() {
        return this.someField;                                // (7)
    }
}

复制代码

确实,当线程1执行到 (5) 的时候,线程2在 (2) 这儿的判断可能会为false。 那线程2就会拿到不是nullinstance

那这个instance为什么会没有被完全初始化呢?

博客中是这样说的,我用我的语言转述下

因为上述的线程1和线程2的执行路径中,并没有任何的同步块处理,也没有volatile变量,所以没有任何happens-before关系,所以无法推倒出(1)<<(7)

令Ta(1)代表线程a执行指令1,Tb(7)代表线程b执行指令7,≼表示happen-before。 我们的目的是想证明Ta(1)≼Tb(7)不成立。首先显然Ta(1)≼Ta(5),然后Tb(2)≼Tb(6)≼Tb(7),因为这都是一个线程内部传递的(规则1+规则8)。 那Ta(5)≼Tb(2)成立吗?可以看到Ta(5)后面有个unlock,那么就可以尝试匹配规则2,然而Tb(2)前面并没有lock,除此之外也没有其他规则可以匹配了,所以Ta(5)≼Tb(2)不成立。 最终结论就是,Ta(1)≼Tb(7)不成立。

上面一段引用是原博中的说明,没有问题。

下面一段引用是博客的一个评论。这个评论把happens-before变成了枯燥的数学问题。想用数学来证明happens-before关系不存在,这样不仅容易出错,而且让人的思维脱离了JMM原理本身,变成了使用公式的计算器,认识不到本质。

其实从本质上来说,这里有两个问题。

  1. 理论上(5)其实分为3个步骤,申请内存+拿到引用->执行(1)->对instance引用的赋值。这两个操作可能被指令重排。其中第二步和第三步可能会被指令重排。就是先对instance引用赋值,然后再执行(1)。
  2. 假如没有发生指令重排,那就没问题了吗?答案是否定的。即使线程1把上面提到的3个步骤都执行完毕,线程2也可能看不到1的值。因为线程1写的仅仅是工作内存,线程2读的之后未必是最新值。

博客中也提到了解决办法

就是把instance按照如下声明

private volatile static LazySingleton instance;
复制代码

volatile为啥能解决问题? 原来我以为,对修饰为volatile的变量的写会立即同步回主内存,而对volatile变量的读会从主内存拿最新值,现在想想并不准确。

instance是个啥,是个reference类型。确切地说,就是个地址,32bit或者64bit。如果volatile仅仅是把引用的可见性保证的话,对对象成员的修改并不能保证可见性。这也是很多对象成员也不得不用volatile修饰的原因。

volatile究竟做了什么?

其实是编译器在对volatile变量的写操作之后加入了同步主存的指令,这个指令不仅仅作用于volatile修饰的变量本身,而且作用于这个语句的所有step涉及的变量。这样就间接地保证了,volatile修饰的变量,能够保证对象的安全发布。

从语义的角度来说,volatile变量的new操作,是对整个对象的操作,并不是对成员的操作,所以volatile有义务保证new语句的可见性。

还有什么解决办法?

to be done

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值