一、指令重排
什么是指令重排?
java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与 它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排发生阶段?
- 执行器编译阶段
- CPU运行时
指令重排的意义
适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
源码到最终执行的指令序列示意图
指令重排遵循的原则
指令重排遵循 as-if-serial 语义1
指令重排的测试
public class VolatileReOrderSample {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
// 添加volatile关键字禁止指令重排
// private static volatile int a = 0, b = 0;
static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
// 这个值需要根据自己的机器去试,尽可能的使两个线程的代码执行时间一致
shortWait(23500);
a = 1;
// 如果不添加volatile关键字,可以通过UnSafe添加内存屏障
x = b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
/*
* 由于a = 1;b = 1;操作分别在 x = b; y = a;操作的前边,正常情况下是不可能出现(0,0)这种情况
*
* 出现这种情况说明y = a;或者x = b;在b = 1;或者a = 1;之前执行的
*
* 也就是说CPU或者JIT进行了指令重排
*/
String result = "第" + i + "次 (" + x + ", " + y +")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
private static void shortWait(int interval) {
long start = System.nanoTime();
long end = start;
while (end - start < interval) {
end = System.nanoTime();
}
}
}
添加volatile关键字,由于volatile禁止指令重排,上述程序中的for循环不再break;
volatile通过内存屏障实现禁止指令重排。
内存屏障
内存屏障:又称内存栅栏,是一个CPU指令
内存屏障的作用
- 保证特定操作的执行顺序,
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
JMM针对编译器制定的volatile重排序规则表
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后;
- ∙当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前;
- ∙当第一个操作是volatile写,第二个操作是volatile读时,不能重排序;
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数 几乎不可能。为此,JMM采取保守策略。
下面是基于保守策略的JMM内存屏障插入策略。 ∙
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障;
as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义 ↩︎