重排序
首先,什么是重排序❓计算机在执行过程中,为了提高性能,会对编译器和编译器做指令重排。
这么做为啥可以提高性能呢❓
我们知道计算机在执行的时候都是一个个指令去执行,不同的指令可能操作的硬件不一样,在执行的过程中可能会产生中断,打个比方,两个指令a和b他们操作的东西各不相同,如果加载a的时候停顿了,b就加载不到,但是实际上它们互补影响,我也可以先加载b在加载a,所以指令重排是减少停顿的一种方法,这样大大提高了效率。
指令重排的方式
指令重排一般分为以下三种:
- 编译器优化重新安排语句的执行顺序。
- 指令并行重排利用指令级并行技术将多个指令并行执行,如果指令之前没有数据依赖,处理器可以改变对应机器指令的执行顺序。
- 内存系统重排由于处理使用缓存和读写缓冲区,所以它们是乱序的。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致**。所以在多线程下,指令重排序可能会导致一些问题。
顺序一致性模型
顺序一致性模型是一个「理论参考模型」,内存模型在设计的时候都会以顺序一致性内存模型作为参考。
数据竞争
我们知道在多线程情况下,同时读写一个变量会导致结果的不确定性,这就存在了数据竞争,相反的如果线程在同步情况下,就不存在数据竞争。
JMM对于同步的多线程情况下,程序执行可以保证顺序一致性,同步包括了使用volatile、final、synchronized等关键字来实现「多线程下的同步」,这里的前提正确使用它们,如果使用不当,就不能保证
什么是顺序一致性模型
我们在上节给大家讲了Java的内存模型,提到了内存可见性的概念,顺序一致性模型它的最终目的就是保证内存的可见性。
它主要有两大特性:
- 一个线程中的所有操作必须按照程序的顺序(代码顺序)来执行。
- 不管线程是否同步,所有线程保持单一的执行顺序并且可见,且是原子性
JMM中同步的顺序一致性
在JMM中,临界区(同步方法或同步块)的代码可以发生重排,但对其它线程是无感知的,这样既提高了执行效率又不影响最终结果
JMM中未同步的顺序一致性
- JMM没有保证未同步程序的执行结果与该程序在顺序一致性中执行结果一致。
- JMM不保证单线程内的操作会按程序的顺序执行(因为指令重排)。
- JMM不保证所有线程能看到一致的操作执行顺序(因为不能保证所以操作立即可见)。
- JMM不保证对64位的long型和double型变量的写操作具有原子性。
什么是happens-before
JMM提供了「happens-before规则」(JSR-133规范), 开发者可以遵循这种规范编写程序,可以保证程序在JMM中具有强的内存可见性。JMM使用happens-before的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程以内,也可以是不同的线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
happens-before关系的定义如下:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 「两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。」
总之,「如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的,不管它们在不在一个线程。」
在Java中,有以下天然的happens-before关系:
- 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start规则:ThreadA start happens-before ThreadB start
- join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
结束语
本节内容可能不像之前那么好理解,比较抽象,所以本文也有不足的地方,大家自己可以多查查一些资料,综合理解。下一节,带大家深入学习一下Java的volatile。