Java内存模型(Java Memory Model,简称JMM)是围绕着在并发过程中如何处理原子性,可见性和有序性这三个特征来建立的;
其中关于JMM中的有序性这一特性的处理,在《深入理解Java虚拟机》12.3.6先行发生原则有这么一段话,如下:
从JDK 5开始,Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,由于这两个操作可以在一个线程之内,也可以是在不同线程之间;因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证;
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系;
参考:[https://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf]
[https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html]
Happens-Before原则
- 如果一个操作Happens-Before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;即如果A happens-before B,那么JMM可以保证A操作的结果对B可见,且A的执行顺序排在B之前;
伪代码如下:
// 以下操作在线程A中完成 i = 1; // 以下操作在线程B中完成 j = i;
假设线程A中的操作“i = 1”先行发生于线程B的操作”j = 1“,那么我们就可以确定在线程B的操作执行后,变量j的值一定等于1;
根据先行发生原则,线程A的”i = 1“的结果可以被线程B观察到,还有就是线程A结束后,没有其他线程会修改变量i的值;
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行;如果重排序之后的执行结果,与按Happens-Before关系来执行的结果一致,那么这种重排序并不非法,JMM允许这种重排序,即JMM对编译器和处理器重排序的约束原则;
摘自《Java并发编程的艺术》的3.2.3 程序顺序规则
伪代码如下:
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
这个例子存在3个happens-before关系,如下:
A happens-before B B happens-before C A happens-before C
在这三个happens-before关系中,第2个和第3个的对执行结果顺序依赖是必须的,而第1个不是必要的;
注:这里的提到的两个操作可以是在一个线程内,也可以是在不同的线程内;
重排序
重排序分为下面两类
- 会改变程序执行结果的重排序;
- 不会改变程序执行结果的重排序;
JMM对于这两种不同性质的重排序,采取了不同的策略,如下:
- 对于会改变程序执行结构的重排序,JMM要求编译器和处理器必须禁止这种重排序;
- 对于不会改变程序执行结构的重排序,JMM对编译器和处理不做要求(JMM允许这种重排序);
as-if-serial:
不管怎么重排序(编译器和处理器为了提高并行度),单线程执行的程序结果不能被改变(as-if-serial语义保护单线程程序);编译器,runtime和处理器都必须遵守as-if-serial语义;
例子如下:
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
这个例子存在3个happens-before关系,如下:
A happens-before B B happens-before C A happens-before C
在这三个happens-before关系中,A和C之间存在数据依赖关系,同时B和C之前也存在数据依赖关系,因此在最终执行的指令序列中,C不能被重排序到A和B前面;
先行发生规则
- 程序次序规则
- 在一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作,即前一个操作的结果可以被后续的操作获取;如:前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1;
- 管程锁定规则
- 同一个锁,一个unlock操作先行发行于后面(这里的“后面”指时间上的前后)对同一个锁的lock操作;
- volatile变量规则
-
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面(这里的“后面”是指时间上的先后)的读是可见的;
-
- 线程启动规则
- Thread对象的start方法先行发生于此线程的每一个动作;
- 线程终止规则
- 线程中的所有操作都先行发生于对此线程的终止检测,通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行;
- 线程中断规则
- 对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread#interruptred方法检测是否有中断发生;
Thread#interrupted静态方法,判断线程是否被中断,并清除当前中断状态,这个方法做了两件事:
- 返回当前线程的中断状态;
- 将当前线程的中断状态设为false;
Thread#interrupt方法仅仅是设置线程的中断状态为true,不会停止线程;
其中,当对一个线程调用Thread#interrupt方法时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响,Thread#interrupt并不能真正的中断线程,需要被调用的线程自己进行配合才行;如果线程处于被阻塞状态(例如调用sleep,wait,join等方法),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常;
- 对象终止规则
- 一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize方法的开始;即对象没有完成初始化之前是不能调用finalized方法的;
- 传递性
- 如果操作A先发生于操作B,操作B先发生于操作C,那么就可以得出操作A先行发生于操作C的结论;
如果代码中两个操作之间的关系不在此列,并无法从以上规则推导出来,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序;