JMM之happens-before详解

一、摘要

 在读完《Java内存模型》这一篇文章之后,我们知道了Java内存模型(JMM)是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会怼代码指令重排序、处理器会怼代码乱序执行等带来的问题。目的是保证并发场景中的原子性、可见性和有序性(这个也是并发编程中,为了保证数据的安全,需要满足的三个特性)。JMM处理并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
 我们也知道,由于CPU的三级缓存与内存通信的架构可能导致内存可见性问题;而且在执行程序时为了提高性能,编译器和处理器的优化:指令重排序,也可能导致多线程内存可见性的问题和有序性问题。
那如何解决内存可见性的问题? 这个便是happens-before规范存在的重要意义,而且happens-before也是JMM中最核心的概念,对于Java程序员来说,理解happens-before是理解JMM的关键。后面我们将分别从JMM的设计,happens-before的定义和规则三个方面详细进行解析。


二、happens-before的设计

 JMM的设计者在设计JMM的时候需要考虑两个关键因素:

  • 程序员对内存模型的使用。程序员希望内存模型易于理解、更能简化编程。程序员希望能基于一个强内存模型来编写代码。
  • 编译期和处理器对内存模型的实现。编译期和处理器希望内存模型对它们的约束越少越好,这样它们就可以做尽可能多的优化来提高性能。编译期和处理器希望实现一个弱内存模型。

 由于这两个因素互相矛盾,所以JSR-133专家组在设计JMM时的核心目标就是找到一个好的平衡点:一方面,要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能地放松。下面我们举例来看下如何实现该目标:

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.
     在3个happens-before关系中,2和3是必需的,但1是不必要的。因此,JMM把happens-before要求禁止的重排序分为了下面两类。
  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序。

 JMM对这两种不同性质的重排序,采取了不同的策略,如下:

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。

 JMM的设计示意图:


JMM设计示意图.jpg

 从图中我们可以得出以下两点结论:

  • JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。
  • JMM对编译期和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当做一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。

三、happens-before的定义

 JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
 《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下:

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

 上面的1是JMM堆程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证------A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
 上面的2是JMM对编译器和处理器重排序的约束原则。正如前面所说,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。

  • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

四、happens-before规则

 《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则:

    1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
    1. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
    1. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    1. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C.
    1. start()规则:如果线程A执行操作ThreadB.start() (启动线程B),那么A线程的ThreadB.start() 操作 happens-before 于线程B中的任意操作。
    1. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before 于线程A 从ThreadB.join() 操作成功返回。

 我们举例来解释以上规则:


single thread HB

synchronized HB

volatile HB

传递性 HB

传递性 HB

五、总结

 JMM提供的happens-before规则,极大的简化了我们对多线程的代码的编写;为我们规避了许多编译器和处理器进行的指令重排以及cpu三级缓存架构导致的内存可见性以及程序执行顺序等各种问题导致的程序在多线程场景的不正确性。
 从内存抽象结构来说,可能出在数据“脏读”的现象,这就是数据可见性的问题,另外,重排序在多线程中不注意的话也容易存在一些问题,比如一个很经典的问题就是DCL(双重检验锁),这就是需要禁止重排序,另外,在多线程下原子操作例如i++不加以注意的也容易出现线程安全的问题。但总的来说,在多线程开发时需要从原子性,有序性,可见性三个方面进行考虑,多从happens-before的规则上面来考虑自己编写的代码是否内存可见等等。


参考引用:

《Java并发编程的艺术》
https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容