重排序与内存语义
重排序
什么是重排序
编译器和处理器为了提高程序的运行性能,对指令进行重新排序
数据依赖性(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。
- 但是事实并非如此,因为a = 1与flag = true之间是没有数据的依赖性的,因此会进行重排序,很可能会先执行flag = true,那么这时候也会进if判断,但此时还没有执行a = 1,因此b的可能是0 + 1 = 1
- 还有可能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引用的对象的成员域的写入,与随后在构造方法外把这个被构造对象的引用赋值给一个引用变量,这个两个操作之间不能重排序