且看一段测试代码, 在不借助外界工具的条件下得出你本身的答案。java
import java.util.*;
import java.util.concurrent.CountDownLatch;
public class Reordering {
static int a = 0;
static int b = 0;
static int x = 0;
static int y = 0;
static final Set<Map<Integer, Integer>> ans = new HashSet<>(4);
public void help() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(2);
Thread threadOne = new Thread(() -> {
a = 1;
x = b;
latch.countDown();
});
Thread threadTwo = new Thread(() -> {
b = 1;
y = a;
latch.countDown();
});
threadOne.start();
threadTwo.start();
latch.await();
Map<Integer, Integer> map = new HashMap<>();
map.put(x, y);
if (!ans.contains(map)) {
ans.add(map);
}
}
@Test
public void testReordering() throws InterruptedException {
for (int i = 0; i < 20000 && ans.size() != 4; i++) {
help();
a = x = b = y = 0;
}
help();
System.out.println(ans);
}
}
复制代码
你的结果ans多是[{0=>1}, {1=>1}, {1=>0}]
, 由于线程调度是随机的, 有可能一个线程执行了, 另一个线程才得到cpu的执行权, 又或者是两个线程交叠执行, 这种状况下ans的答案无疑是上面三种结果, 至于上面三种结果对应的线程执行顺序, 我这里就不模拟了, 这不是重点。可是其实ans除了上面的三种结果以外, 还有另一种结果{0=>0}
, 这是为何呢? 要想出现{0=>0}这种结果无非就是:安全
x = b happens before a = 1
呢? 这其实就是指令重排序。大多数现代微处理器都会采用将指令乱序执行的方法, 在条件容许的状况下, 直接运行当前有能力当即执行的后续指令, 避开获取下一条指令所需数据时形成的等待。经过乱序执行的技术, 处理器能够大大提升执行效率。除了cpu会对指令重排序来优化性能以外, java JIT也会对指令进行重排序。app
那么何时不由止指令重排序或者怎么禁止指令重排序呢?否则一切都乱套了。函数
其一, 有数据依赖关系的指令不会进行指令重排序! 什么意思呢?工具
a = 1;
x = a;
复制代码
就像上面两条指令, x
依赖于a
, 因此x = a
这条指令不会重排序到a = 1
这条指令的前面。性能
有数据依赖关系分为如下三种:测试
a = 1
和x = a
, 这就是典型的写后读, 这种不会进行指令重排序。a = 1
和a = 2
, 这种也不会进行重排序。x = a
和a = 1
。什么是as-if-serial? as-if-serial语义就是: 无论怎么重排序(编译器和处理器为了提升并行度), 单线程程序的执行结果不能被改变。因此编译器和cpu进行指令重排序时候回遵照as-if-serial语义。举个栗子:优化
x = 1; //1
y = 1; //2
ans = x + y; //3
复制代码
上面三条指令, 指令1和指令2没有数据依赖关系, 指令3依赖指令1和指令2。根据上面咱们讲的重排序不会改变咱们的数据依赖关系, 依据这个结论, 咱们能够确信指令3是不会重排序于指令1和指令2的前面。咱们看一下上面上条指令编译成字节码文件以后:this
public int add() {
int x = 1;
int y = 1;
int ans = x + y;
return ans
}
复制代码
对应的字节码spa
public int add();
Code:
0: iconst_1 // 将int型数值1入操做数栈
1: istore_1 // 将操做数栈顶数值写到局部变量表的第2个变量(由于非静态方法会传入this, this就是第一个变量)
2: iconst_1 // 将int型数值1入操做数栈
3: istore_2 // 将将操做数栈顶数值写到局部变量表的第3个变量
4: iload_1 // 将第2个变量的值入操做数栈
5: iload_2 // 将第三个变量的值入操做数栈
6: iadd // 操做数栈顶元素和栈顶下一个元素作int型add操做, 并将结果压入栈
7: istore_3 // 将栈顶的数值存入第四个变量
8: iload_3 // 将第四个变量入栈
9: ireturn // 返回
复制代码
以上的字节码咱们只关心0->7行, 以上8行指令咱们能够分为:
上面的5个操做, 1操做和二、4可能会重排序, 2操做和一、3ch重排序, 操做3可能和二、4重排序, 操做4可能和一、3重排序。对应上面的赋值x和赋值y有可能会进行重排序, 对, 这并不难以理解, 由于写x和写y并无明确的数据依赖关系。可是操做1和3和5并不能重排序, 由于3依赖1, 5依赖3, 同理操做二、四、5也不能进行重排序。
因此为了保证数据依赖性不被破坏, 重排序要遵照as-if-serial语义。
@Test
public void testReordering2() {
int x = 1;
try {
x = 2; //A
y = 2 / 0; //B
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(x);
}
}
复制代码
上面这段代码A和B是有可能重排序的, 由于x和y并无数据依赖关系, 而且也没有特殊的语义作限制。可是若是发生B happens-before A的话, 此时是否是就打印了错误的x的值, 其实否则: 为了保证as-if-serial语义, Java异常处理机制对重排序作了一种特殊的处理: JIT在重排序时会在catch语句中插入错误代偿代码(即重排序到B后面的A), 这样作虽然会致使catch里面的逻辑变得复杂, 可是JIT优化原则是: 尽量地优化程序正常运行下的逻辑, 哪怕以catch块逻辑变得复杂为代价。
这就是happens-before传递性
Java内存模型(Java Memory Model简称JMM)总结了如下8条规则, 保证符合如下8条规则, happens-before先后两个操做, 不会被重排序且后者对前者的内存可见。
有人确定写过下面的double-check单例模式
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
复制代码
可是这种double-check加锁的单例是正常的吗? No. 由于建立一个实例对象并非一个原子性的操做, 并且还可能发生重排序, 具体以下: 假定建立一个对象须要:
上面的2和3操做是有可能重排序的, 若是3重排序到2的前面, 这时候2操做尚未执行, instance已经不是null了, 固然不是安全的。
那么怎么防止这种指令重排序? 修改以下:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
复制代码
volatile
关键字有两个语义: 其一保证内存可见性, 这个语义咱们下次博客会讲到(其实就是一个线程修改会对另外一个线程可见, 若是不是volatile
, 线程操做都是在TLAB有副本的, 修改了副本的值以后不即时刷新到主存, 其余线程是不可见的) 其二, 禁止指令重排序, 若是上面new
的时候, 禁止了指令重排序, 因此能获得指望的状况。
题外话, 关于线程安全的单例, 每每能够采用静态内部类的形式来实现, 这种无疑是最合适的了。
public class Singleton {
public static Singleton getInstance() {
return Helper.instance;
}
static class Helper {
private static final Singleton instance = new Singleton();
}
}
复制代码
咱们以前一会容许重排序, 一会禁止重排序, 可是重排序禁止是怎么实现的呢? 是用内存屏障cpu指令来实现的, 顾名思义, 就是加个障碍, 不让你重排序。
内存屏障能够被分为如下几种类型: