并发 - volatile(二)

2019-12-21  本文已影响0人  sunyelw

问题是最好的导师, 细心是最棒的品质

DCL一定要加volatile吗?


一、DCL

DCL全称double check lock, 双重检查锁
这是单例模式的一种实现, 也是目前的标配实现, 在这之前还有饿汉/懒汉等实现, 先看一下DCL的实现

public class DclSingleBean {

    private volatile static DclSingleBean instance;

    private DclSingleBean() {
        System.out.println("doSomeThing...");
    }

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

这是一种安全的懒汉式单例实现, 很经典的一种设计模式.

二、再谈volatile

上篇文章讲了些volatile的可见性, 还有一个很耀眼的特性, 叫做禁止指令重排

0. JMM - Java内存模型

在 Java内存模型中规定了三种特性

以下引用自 Java内存模型与指令重排

如果光靠sychronizedvolatile来保证程序执行过程中的原子性, 有序性, 可见性, 那么代码将会变得异常繁琐.

JMM提供了Happen-Before规则来约束数据之间是否存在竞争, 线程环境是否安全, 具体如下:

1. 什么叫指令重排

我们有三行代码

1  int a = 1;
2  int b = 2;
3  int c= a + b;

正常的执行方式就是顺序执行,但我们会发现第一行与第二行是没有什么前后依赖关系的,那么如果把第一行与第二行同时执行不就可以提升运行速度吗?

于是JVM的建设者就做了类似的优化,这种优化就是指令重排,顾名思义就是对指令的执行顺序按照一定的规则进行重新排序以使得其运行更快

JVM中做这件事的是JIT <Just In Time Compiler> 即时编译器

在部分的商用虚拟机中,Java 程序最初是通过解释器( Interpreter )进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为热点代码。为了提高热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler)会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。

参考 https://www.cnblogs.com/linghu-java/p/8589843.html

所以上述代码执行时可能就被优化为第一行与第二行同时执行, 然后再执行第三行.

2. 指令重排带来的问题

很多情况下的指令重排会加快运行效率,但一些特殊情况下的指令重排却可能带来一些难以定位的问题,其中 DCL 就是比较典型的一种.

为了方便解析DclSingleBean类的实例化过程,我们加一个字段

public class DclSingleBean {

    private int x;
    
    public int getX() {
        return x;
    }
    
    private volatile static DclSingleBean instance;

    private DclSingleBean() {
        x = 8;
        System.out.println("doSomeThing...");
    }

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

DclSingleBean类的实例化过程可以简化为以下步骤

private volatile static DclSingleBean instance;
private DclSingleBean() {
    x = 8;
}
instance = new DclSingleBean();

如果允许重排序, 也就是去掉volatile关键字, 假设第三步构造与第四步建立转换的执行顺序发生调换会发生什么?

在建立连接之后, 有其他线程此时进入第一重的null == instance判断, 得到的是false, 因为已经建立连接了. 然后直接返回了这个半初始化的对象instance, 拿到其中的x值为0, 这里的问题就暴露出来了.

说明

这是volatile的禁止指令重排的经典例子, 可以好好琢磨下, 容易陷入的误区是我既然加锁了, 为什么还有其他线程能拿到当前线程还没有实例完全的对象?

3. 禁止指令重排 - 内存屏障

我理解内存屏障就是指令之间的高墙, 禁止逾越.
而只有读/写才有顺序之分, 分别是以下指令

而对应地有四种内存屏障

StoreStoreBarrier就是写与写之间的内存屏障, 保证了屏障之前的store操作发生于屏障之后的store, 其他三个类似.

StoreLoad Barriers同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其他屏障,该屏障的开销相对昂贵。

拓展一下操作系统级别的实现指令

详细可参见内存屏障详解

4. 指令重排的实例

贴一段代码, 来源Memory Reordering Caught in the Act, 使用Java语言实现了一遍.

public class CatchReOrderClass {

    private static int a, b, x, y;

    public static void main(String[] args){

        int i = 0;
        for (;;) {
            a = 0; b = 0;
            x = 0; y = 0;
            new Thread(() -> {
                a = 1;
                x = b;
            }).start();

            new Thread(() -> {
                b = 1;
                y = a;
            }).start();

            i++;
            if (x == 0 && y == 0) {
                System.out.println(i);
                break;
            }
        }
    }
}

执行次数足够多的情况下会结束循环, 此时表示发生了指令重排.

上一篇 下一篇

猜你喜欢

热点阅读