原创

Java内存模型的指令重排序和happen-before

温馨提示:
本文最后更新于 2021年04月09日,已超过 606 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

happen-before 和 指令重排序不是一个概念。

要清楚happen-before ,首先要知道Java内存模型。

Java内存模型参考:https://rain.baimuxym.cn/article/15

指令重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序

所以说我们书写代码的顺序,并不是等同于代码在CPU真正执行的顺序。

这些重排序会导致线程安全的问题,一个很经典的例子就是双重锁定检查(DCL)。

DCL问题可以看看: https://www.jianshu.com/p/ca19c22e02f4

JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。

内存屏障

编译器和处理器都必须遵守重新排序规则。不需要特别的努力来确保单处理器保持适当的顺序,因为它们都保证“按顺序”一致性。但是在多处理器上,要保证一致性,通常需要发出屏障指令。即使编译器优化了字段访问(例如,因为未使用加载的值),也必须仍然生成屏障,就像访问仍然存在一样。

内存屏障的概念以及:http://gee.cs.oswego.edu/dl/jmm/cookbook.html

指令重排序可以分为三种:

(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
(2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

比如说:

由于A,B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。

以上部分参考自:https://blog.csdn.net/ma_chen_qq/article/details/82990603

happen-before

happen-before 是 Java 内存模型中保证多线程操作可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义。

内存模型通过happen-before 关系向程序员提供跨线程的内存可见保证性(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。

具体的定义为:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

happen-before规则一共就八条:

1、单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。

比如说:

int a = 3;      //1
int b = a + 1;  //2

这里 //1对变量a的赋值操作对//2一定可见。

因为//2 中有用到//1中的变量a,再加上java内存模型提供了“单线程happen-before原则”,所以java虚拟机不许可操作系统对//1 //2 操作进行指令重排序。

如果是:

int a = 3;
int b = 4;

指令重排序则可能发生。

2、锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

常见的就是synchronized、reentrantLock加锁。

解锁操作的结果对后面的加锁操作一定是可见的,无论两个是否在一个线程;简单的说就是线程A加了锁,在我没有解锁之前,线程B是无法进入的,而当线程A解锁了,线程B是可以感知的。

例子就不举了,前面的synchronized文章有说到。

3、volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。

对 volatile 变量的写操作的结果对于发生于其后的任何操作的结果都是可见的。x86 架构下volatile 通过内存屏障缓存一致性协议实现了变量在多核心之间的一致性。

volatile int a;
//线程A执行
a = 1; //1
//线程B执行
b = a;  //2

如果线程A 执行//1,线程B执行了//2,并且“线程A”执行后,“线程B”再执行,那么符合“volatile的happen-before原则”所以“线程2”中的a值一定是1,而不会是初始值0。

4、happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

可以根据这两个规则推导出两个没有直接联系的操作其实是存在happen-before 关系的。

5、 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

翻译成人话就是:我在main线程 赋值了一个字段,下一步我又通过start()启动一个线程,那么这个main线程赋值操作对子线程是可见的。

int a ;
//main线程
a = 1;
new Thread().start();

//子线程
b = a // b等于1

6、线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

7、 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测,又叫做join规则

这两点可以理解为线程之间的通信,主线程对子线程发出通知,子线程是可以感知的。同时主线程可以感知子线程的状态。

8、 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

即先有类的初始化才有销毁,感觉这个和类加载器有关系。


happen-before,它不能简单地说前后关系,是因为它不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。

它和时间没有任何关系,仅仅是时钟顺序上的先后,并不能保证线程交互的可见性。

对于学习JMM,个人觉得不要陷得太深,毕竟这东西和CPU打交道,纠结于这些复杂的东西,未必有价值。

正文到此结束
关注公众号 【HelloCoder】
免费领取Java学习资料
让技术,化繁为简
本文目录