文章目录
一、有序性问题深入分析
思考:下面的Java程序中x和y的最终结果是什么?
public class ReOrderTest {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException{
int i=0;
while (true) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
/**
* x,y:
*/
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(20000);
a = 1;
x = b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("第" + i + "次(" + x + "," + y + ")");
if (x == 0 && y == 0){
break;
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
二、指令重排序
ava语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义: JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
在编译器与CPU处理器中都能执行指令重排优化操作。
2.1 volatile重排序规则
volatile禁止重排序场景:
1、第二个操作是volatile写,不管第一个操作是什么都不会重排序
2、第一个操作是volatile读,不管第二个操作是什么都不会重排序
3、第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序
2.2 JMM内存屏障插入策略
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
2.3 JSR133规范
https://blog.csdn.net/qq_43631716/article/details/114760621
86处理器不会对读-读(LoadLoad)、读-写(LoadStore)和写-写(StoreStore)操作做重排序, 会省略掉这3种操作类型对应的内存屏障。仅会对写-读(StoreLoad)操作做重排序,所以volatile写-读操作只需要在volatile写后插入StoreLoad屏障。
三、深入理解内存屏障
3.1 JVM层面的内存屏障
在JSR规范中定义了4种内存屏障:
1、LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
2、LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
3、StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
4、StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作。
3.2 硬件层内存屏障
硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。
拿X86平台来说,有几种主要的内存屏障:
1、lfence,是一种Load Barrier 读屏障
2、sfence, 是一种Store Barrier 写屏障
3、mfence, 是一种全能型的屏障,具备lfence和sfence的能力
4、Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。 它后面可以跟ADD, ADC,AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
内存屏障有两个能力:
1、阻止屏障两边的指令重排序
2、刷新处理器缓存/冲刷处理器缓存
对Load Barrier(Ifence)来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;
对Store Barrier(sfence)来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。