Java 内存模型中happens-before的理解

来源:互联网 发布:excel表格制作软件 编辑:程序博客网 时间:2024/06/11 02:38

关于Java同步中的可见性,顺序性,原子性在java内存模型和java语言规范的相关文档中都有详细的说明,但是老外的文档写得都不是人话,使用的概念又很高大上,经过多方面的资料阅读,写一些自己的理解。

  • happens-before 关系的定义

Happens-Before Relationship Two actions can be ordered by a happens-before relationship.If one action happens before another, then the first is visible to and ordered before the second.

到处都没有找到关于happens-before关系的比较清楚的定义,Java Language Specification(jls8)中关于happens-before的描述如上,没有给出正式的定义,只是给出了如果两个动作有happens-before关系之后,会产生什么效果。

如果两个动作之间有happens-before关系,那么第一个动作对第二个动作是可见的并且排序在第二个动作前面(应该是不允许重排序到第二个动作后面的意思)

  • happens-before 关系的作用
happens-before会保证两个效果:可见性和顺序性
  • 没有happens-before 关系会怎么样

如上所述,happens-before关系保证两个效果:可见性和顺序性,那么没有happens-before关系就不保证这两个效果,那么这两个效果是干什么的,没有它们又会怎么样

可见性,就是一个动作产生的效果对另一个动作来说是不是可见的,或者说能不能被另一个动作看到,主要描述的是一个写能不能被一个读看到写的值。

为什么还会存在这种问题了,写完值后,读不是能读到吗,这会有什么疑问吗?什么会导致我写了一个值,再去读却读不到呢?

导致读不到的情况有两种:一个是写的值没有及时的刷新到主内存,另一个是重排序了。

写的值没有刷新到主内存:由于cpu跟内存在速度上的巨大差异,如果每次cpu都直接去读内存,对cpu来说是巨大的浪费,需要相对长时间的等待,所以各个cpu都有了缓存,寄存器之内的可以用于缓存住内存中的内容,通常称为工作内存,实现快速访问。但是这样就导致了一个问题,工作内存中的缓存内容的改变并不保证能及时的刷新到住内存,也不能保证每次住内存被其它线程修改了,工作内存能及时从住内存重新加载。所以如果线程A修改了共享变量的值v,线程2在之后读v的值,在没有happens-before保证的情况下,线程2读到的可能是线程1写入的新值,也可能是线程1写之前,其它线程写入的值。

class LoopMayNeverEnd {boolean done = false;void work() {while (!done) {// do work}}void stopWork() {done = true;}}
线程1线程2
work()
stopWork()
如上代码,线程1执行word(),通过判断是否结束的标志done来判断是否结束循环;线程2执行stopWork()来结束,设置done为true。按照预期,线程b执行后,线程a也会从循环中结束。但实际的情况可能是线程a永远无法结束。

一    因可能是编译优化(或处理器优化)之后,循环条件可能会被提出,循环直接变成一个无限循环,能这样做的原因在单线程中看不到done能被修改,所有!done永远为tue,循环被优化成无限循环。

线程b对done的改变并没有及时刷新到住内存,或是线程a的后续的循环判断没有从住内存重新加载done的值(很可能线程a不会再去重新加载,应为它看不到有哪里更新过这个值,所以可以一直用第一次读到的值,后续一直从自己的工作内存中读),只有线程b及时刷新住内存并且之后线程a从主内存重新加载值两个都发生,线程a才能从循环中正常退出。

循环不退出的结果明显不符合我们的预期,所以我们需要一个机制让线程1能及时看到线程2对done的写,对于这个案例,通常给出的解决办法是把done声明为volatile的,那么这是怎么解决的呢;

在jsr133(很早的事了,2004年的,可以认为我们现在是有的jre,jdk都符合这个规范)修正后的内存模型里,volatile变量的读写有如下效果There is a happens-before edge from a write to a volatile variable v to all subsequent reads of v by any thread,即对于volatile变量的写与后续的对于同一个变量的读之间有一个happens-before的关系,保证了可见性。一个线程对volatile变量写之后,会强制要求把该线程的工作内存中的内容刷新到主内存;一个线程读取volatile变量之前,会使该线程的工作内存失效,强制从主内存重新加载,从而保证可见性。

当然,volatile还有一个禁止重排序的语义,后面在详细说

顺序性,就是编译器或处理器可以对代码的执行顺序做优化,导致实际的执行顺序跟代码的顺序并不一致

class BadlyOrdered {boolean a = false;boolean b = false;void threadOne() {a = true;b = true;}boolean threadTwo() {boolean r1 = b; // sees trueboolean r2 = a; // sees falsereturn r1 && !r2; // returns true}}
线程1线程2theadOne()theadTwo()共享变量a和b初始值为false,线程1执行threadOne(),线程2执行threadTwo(),结果线程2中的结果可能是r1读到b的值已经为true,但是同时r2读到a的值还是false,所以线程2执行的结果可能返回为true,只是不符合预期的。这是因为线程1中的两个赋值语句可能会被重排序,先执行了b的赋值,然后线程2读到了值,最后才是a的赋值。

那么什么条件下,优化能调整代码的顺序,即重排序呢?

编译器和处理器对代码的优化有非常大的自由,注意是非常大。它能给我们的保证只有一个,遵守顺序执行的语义,或者说单线程的语义。在单线程下,只要不改变程序的语义,优化可以自由的进行。举个最简单的关于重排序的例子

i=x;j=y;k=i+j;
这里的i,j的赋值就可以重排序,在单线程下,对程序语义没有任何改变。这里的对程序语义是否有改变可以理解为,编译器或处理器做的优化是否能被观察到,如果不能被观察到,则是合理的优化,即没有改变程序的行为表现。所以我们一直没有直观的看到编译器的优化,尤单线程下,优化不会被感知。编译器的优化只会影响多线程下的表现。除了单线程的语义之外,我们对优化不能抱有任何的期待,如果还有什么期待,那就明确的告诉编译器或处理器你的要求,否则你的要求得不到任何保证,而这就是java内存模型要做的事情。

这里把b声明为volatile同样能够解决这个问题,这就是java内存模型赋予volatile变量的另外一个语义,对于重排序的禁止:

禁止把volatile写之前的行为与它重排序

禁止把volatile读之后的行为与它重排序

  • 怎么产生happens-before关系

如下是能产生happens-before关系的行为,其中的hb(x,y)表示x happens-before y,摘自jls8

• If x and y are actions of the same thread and x comes before y in program order,then hb(x, y).

• There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.
• If an action x synchronizes-with a following action y, then we also have hb(x, y).

• If hb(x, y) and hb(y, z), then hb(x, z).

其中,有synchronizes-with关系的行为如下

•An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where "subsequent" is defined according to the synchronization order).
• A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).
• An action that starts a thread synchronizes-with the first action in the thread it starts.

• The write of the default value (zero, false, or null) to each variable synchronizes-with the first action in every thread.
Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.
• The final action in a thread T1 synchronizes-with any action in another thread T2 that detects that T1 has terminated.
T2 may accomplish this by calling T1.isAlive() or T1.join().
• If thread T1 interrupts thread T2, the interrupt by T1 synchronizes-with any point where any other thread (including T2) determines that T2 has been interrupted (by having an InterruptedException thrown or by invoking Thread.interrupted or Thread.isInterrupted).

其中• If hb(x, y) and hb(y, z), then hb(x, z).,和• If x and y are actions of the same thread and x comes before y in program order,then hb(x, y).两条规则。

个人理解,首先 If x and y are actions of the same thread and x comes before y in program order,then hb(x, y) ,单线程中前面的行为happens-before行为,然后happens-before关系又是可传递的,那么对于其它的happens-before关系来说,一定会实现相关的禁止重排序的语义,否则,保证不了第一条规则的传递性。


0 0