并发知识 -- 指令重排
本文最后更新于:5 天前
为了更好地利用CPU,CPU和编译器都可能会对指令进行重排(reordering):
-
编译期间重排: 编译器在编译期间,可能对指令进行重排,以使其对 CPU 更友好
-
指令并行重排(运行期间重排):CPU 在执行指令的过程中,可能乱序执行以更好地利用流水线
-
内存系统重排:由于 CPU 使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
不管是 CPU 架构,还是编译器,在对指令进行重排时都要遵守一个约束: 不管指令如何重排,对单线程来说,结果必然是一致的。即不会改变单线程程序的行为。(as-if-serial 原则)
那么,为了保证 as-if-serial 原则,但是又要最大限度的进行重排序的优化,所以引入了内存屏障和临界区
也就是说,如果没有 happens-before 原则,那么对于代码 JVM 可以去做重排序,对于存在 happens-before 原则中出现的代码 JVM 不能做重排序
⚡禁止指令重排的一个例子
public class Container {
public static class SomeThing {
private int status;
public SomeThing() {
status = 1;
}
public int getStatus() {
return status;
}
}
private SomeThing object;
public void create() {
object = new SomeThing();
}
public SomeThing get() {
while (object == null) {
Thread.yield(); //不加这句话可能会在此出现无限循环
}
return object;
}
}
这个容器并不要求一个线程写入了object,object的新值立即对下一个读取的线程可见,但是这段代码在运行的时候可能还是会出现并发问题,和 DCL 单例类似,可能在加入引用的时候得到不完整的对象,那么我们同样也可以通过加入 volatile 关键字保证可见性
public class Container {
public static class SomeThing {
private int status;
public SomeThing() {
status = 1;
}
public int getStatus() {
return status;
}
}
private volatile SomeThing object;
public void create() {
object = new SomeThing();
}
public SomeThing get() {
while (object == null) {
Thread.yield(); //不加这句话可能会在此出现无限循环
}
return object;
}
}
volatile 修饰 object 字段。这种方法避免了重排序,保证了内存可见性,摒弃比使用同步块导致的性能损失更小。
但是由于我们不要求一个线程写入了object,object的新值立即对下一个读取的线程可见,而且在 Intel 64/IA-32 环境下,有更好的解决方案。
Intel 64/IA-32 下写操作之间不会发生重排序,即在处理器中,构建SomeThing对象与赋值到object这两个操作之间的顺序性是可以保证的。这样看起来,仅仅使用volatile来避免重排序是多此一举的。但是,Java编译器却可能生成重排序后的指令。但令人高兴的是,Oracle的JDK中提供了Unsafe.putOrderedObject,Unsafe.putOrderedInt,Unsafe.putOrderedLong这三个方法,JDK会在执行这三个方法时插入StoreStore内存屏障,避免发生写操作重排序。而在 Intel 64/IA-32 架构下,StoreStore 屏障并不需要,Java编译器会将StoreStore屏障去除。比起写入volatile变量之后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失以外,不会带来其它的性能开销。
本博客所有文章除特别声明外,均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。转载请注明出处!