其中2和3属于CPU执行阶段的重排序,1属于编译器阶段的重排序。编译器会遵守happens-before规则和as-if-serial语义的前提下进行指令重排。
happens-before规则:如果A happens-before B,且B happens-before C,则需要保证A happens-before C。
as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、Runtime和处理器都必须遵守as-if-serial语义。
对于处理器重排序,JMM要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理重排序。
JMM的内存屏障
========
上面了解了CPU的内存屏障分类,在JMM中把内存屏障分为四类:
-
LoadLoad Barriers:示例,Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2及所有后续指令的装载;
-
StoreStore Barriers:示例,Store1;StoreStore;Store2,确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储;
-
LoadStore Barriers:示例,Load1;LoadStore;Store2,确保Load1数据装载先于Store2及所有后续存储指令刷新到内存;
-
StoreLoad Barriers:示例,Store1;StoreLoad;Load2,确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
其中,StoreLoad Barriers同时具有前3个的屏障的效果,但性能开销很大。
为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下图是JMM针对编译器制定的volatile重排序规则表。
JMM重排序
从图中可以得出一个基本规则:
-
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
-
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
-
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:
-
在每个volatile写操作的前面插入一个StoreStore屏