并发知识 -- 指令重排

本文最后更新于:5 天前

为了更好地利用CPU,CPU和编译器都可能会对指令进行重排(reordering):

  • 编译期间重排: 编译器在编译期间,可能对指令进行重排,以使其对 CPU 更友好

  • 指令并行重排(运行期间重排):CPU 在执行指令的过程中,可能乱序执行以更好地利用流水线

  • 内存系统重排:由于 CPU 使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

不管是 CPU 架构,还是编译器,在对指令进行重排时都要遵守一个约束: 不管指令如何重排,对单线程来说,结果必然是一致的。即不会改变单线程程序的行为。(as-if-serial 原则)

那么,为了保证 as-if-serial 原则,但是又要最大限度的进行重排序的优化,所以引入了内存屏障临界区

也就是说,如果没有 happens-before 原则,那么对于代码 JVM 可以去做重排序,对于存在 happens-before 原则中出现的代码 JVM 不能做重排序

禁止指令重排的一个例子

public class Container {
    public static class SomeThing {
        private int status;

        public SomeThing() {
            status = 1;
        }

        public int getStatus() {
            return status;
        }
    }

    private SomeThing object;

    public void create() {
        object = new SomeThing();
    }

    public SomeThing get() {
        while (object == null) {
            Thread.yield(); //不加这句话可能会在此出现无限循环
        }
        return object;
    }
}

这个容器并不要求一个线程写入了object,object的新值立即对下一个读取的线程可见,但是这段代码在运行的时候可能还是会出现并发问题,和 DCL 单例类似,可能在加入引用的时候得到不完整的对象,那么我们同样也可以通过加入 volatile 关键字保证可见性

public class Container {
    public static class SomeThing {
        private int status;

        public SomeThing() {
            status = 1;
        }

        public int getStatus() {
            return status;
        }
    }

    private volatile  SomeThing object;

    public void create() {
        object = new SomeThing();
    }

    public SomeThing get() {
        while (object == null) {
            Thread.yield(); //不加这句话可能会在此出现无限循环
        }
        return object;
    }
}

volatile 修饰 object 字段。这种方法避免了重排序,保证了内存可见性,摒弃比使用同步块导致的性能损失更小。

但是由于我们不要求一个线程写入了object,object的新值立即对下一个读取的线程可见,而且在 Intel 64/IA-32 环境下,有更好的解决方案。

Intel 64/IA-32 下写操作之间不会发生重排序,即在处理器中,构建SomeThing对象与赋值到object这两个操作之间的顺序性是可以保证的。这样看起来,仅仅使用volatile来避免重排序是多此一举的。但是,Java编译器却可能生成重排序后的指令。但令人高兴的是,Oracle的JDK中提供了Unsafe.putOrderedObject,Unsafe.putOrderedInt,Unsafe.putOrderedLong这三个方法,JDK会在执行这三个方法时插入StoreStore内存屏障,避免发生写操作重排序。而在 Intel 64/IA-32 架构下,StoreStore 屏障并不需要,Java编译器会将StoreStore屏障去除。比起写入volatile变量之后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失以外,不会带来其它的性能开销。

例子出处