重排序与内存语义

重排序

什么是重排序

编译器和处理器为了提高程序的运行性能,对指令进行重新排序

数据依赖性(as-if-serial)

虽然重排序能够优化性能,但是前提是必须保证结果正确,那么它是如何在单线程下保证结果正确的呢?是根据诗句的依赖性来做的,数据的依赖性可以分为三类:

  • 写后读

    写后读的操作肯定是不能重排的,例如a = 1然后b = a

    这对于a来说是一个先写后读的过程,这个过程不能改变,如果变成b = a然后a = 1,那么结果也就变了,b最终得到的就不是1

  • 读后写

    读后写也不能重排,例如b = a然后a = 1,对于a来说这是一个读后写的操作,不能重排

    重排后变成a = 1,b = a。那么结果就变了,最终b的值就是1了

  • 写后写

    写后写也是不能重排的,例如a = 1,a = 2。

    如果重排,变成a = 2,a = 1。那么最后a的结果是1而不是2了

指令重排序分类

  • 编译器重排序
  • 处理器重排序

指令重排序带来的影响

在单线程中不会有太大的影响,只会提升执行效率,但是在多线程中指令重排序会有较大的影响

例如:

public class Demo08 {
    private int a;
    private boolean flag;

    public void writer() {
        a = 1;
        flag = true;
    }

    public void read() {
        if (flag) {
            int b = a + 1;
            System.out.println(b);
        }
    }
}

在上面的代码中我们要做的事很明确,就是先将a设置为1,然后flag设置为true。然后再判断flag的值,这时候为true,会进if判断,然后b = a + 1,因此理想的情况下b等于2。

  1. 但是事实并非如此,因为a = 1与flag = true之间是没有数据的依赖性的,因此会进行重排序,很可能会先执行flag = true,那么这时候也会进if判断,但此时还没有执行a = 1,因此b的可能是0 + 1 = 1
  2. 还有可能int b = a + 1会先执行,然后将执行的结果放到某块内存,等到flag为true的时候再取出来,但是早在执行int b = a + 1的时候a的值可能还没改变,那就会有问题

happens-before

happens-beore是用来指定两个操作之间的执行顺序,提供跨线程的内存可见性

happens-before规则如下:

  • 程序顺序规则
    • 单个线程中的每一个操作,总是前一个操作happens-before于该线程中任意后续操作,也就是说如果a happens-before b,那么并不是表示a操作在b操作之前执行,而是表示a操作对于b操作是可见的
  • 监视器锁规则
    • 对于一个锁的解锁总是happens-before于随后对这个锁的加锁
  • volatile变量规则
    • 对于一个volatile域的写,happens-before于任意后续对这个volatile域的读
  • 传递性
    • 如果a happens-before b,而b happens-before c,那么a一定happens-before c
  • Start规则
    • 如果在a线程中调用了b线程,那么在a线程中的所有代码是happens-before b线程的
  • Join规则
    • 如果在a线程中执行了b线程的join()方法,那么b线程happens-before a线程,这与start规则是相反的

内存语义

锁的内存语义

锁的释放与获取所建立的happens-before关系:锁的释放happens-before锁的获取

锁的释放和获取的内存语义:锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程和发送消息

volatile内存语义

volatile读写所建立的happens before关系:对于一个volatile域的写,happens-before于任意后续对这个volatile域的读

volatile读写的内存语义:当写一个volatile变量时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;当读一个volatile变量时,Java内存模型会把当前线程对应的本地内存中的共享变量重置为无效,然后从主内存中读取共享变量

final内存语义

在java中,被final修饰的类不能被继承,被final修饰的方法不能被重写,被final修饰的变量不能被改变

  • 写final域的重排序规则:禁止把final域的写重排序到构造方法之外,也就是可以在构造方法中初始化final修饰的变量

    public class Demo09 {
        private final int b;
        public Demo09() {
            b = 10;
        }
    }
    
    // 编译器会在final域的写之后,在构造方法执行完毕之前,插入一个内存屏障StoreStore,保证处理器把final域的写操作在构造方法之中完成
    
    // 一般的内存屏障
    // LoadLoad
    // StoreStore
    // LoadStore
    // StoreLoad
    
  • 读final域的重排序规则:在一个线程中,初次读对象引用和初次读该对象所包含的final域,Java内存模型禁止处理器重排序这两个操作

  • final域为静态类型:

  • final域为抽象类型:在构造方法内对一个final引用的对象的成员域的写入,与随后在构造方法外把这个被构造对象的引用赋值给一个引用变量,这个两个操作之间不能重排序

posted @ 2019-12-15 21:38  Jin同学  阅读(383)  评论(0编辑  收藏  举报