这是 why 的第 99 篇原创文章
你好呀,我是 why 哥。
不是,这个照片不是我,题目说的老爷子就是这个哥们,这事得从前几天说起。
前几天,发现在一个大佬星散的技术群外面,大佬们就 Happens-Before 关系和 as-if-serial 语义进行了强烈的探讨。
而我一看工夫,快到 23 点了,大佬们都这么卷,那我必须得跟着卷进去,于是看了一下他们的聊天记录。
而我,作为一只菜鸡,尽管没有什么参与感,然而感觉大佬们说的都挺有情理的,据理力争。
所以基本上,我全程就是这样的:
然而,当他们说着说着就聊到了《Java 并发编程实战》,我一下就支棱了起来。
这书我看过啊,而且这书就在我手边呀,终于能够插上话了。
认真一看,他们说的是书中的 16.1.4 大节:
没啥映像了,甚至连“借助同步”这个词都没有搞明确啥意思。
于是我翻到这一大节,读了起来。
因为这大节篇幅不长,且除了 Happens-Before 关系这个基础知识铺垫外,没有其余的背景,所以我把这一大节截图进去,给大家看看:
怎么样,大家看完之后什么感觉?
是不是甚至都没有急躁看完,一种云里雾里的感觉?
说实话,我看的时候就是这个感觉,每个字都看得懂,然而连在一起就不晓得啥意思了。
所以,读完之后的感觉就是:
找源码
然而不慌,文章外面举的例子是 FutureTask,这玩意并发编程根底之一,我熟啊。
于是决定去源码外面看看,然而并没找到书中举的 innerSet 或者 innerGet 的办法:
因为我这里是 JDK 8 的源码了,而这本书的公布工夫是 2012 年 2 月:
因为是译本,原书写作工夫可能就更早了。
比照这 JDK 版本公布工夫线来看,如果是源码,也是 JDK 8 之前的源码了:
果然,一个大佬通知我,JDK 6 外面的源码就是这样写的:
然而我感觉去钻研 JDK 6 的收益不是很大呀。(次要还是我懒得去下载)
于是,我还是在 JDK 8 的源码外面,发现了一点点蛛丝马迹。
终于搞懂了,什么是“借助同步”了。
而且不得不赞叹 Doug Lea 老爷子的代码,真的是:妙啊。
到底什么是“借助同步”呢?且听我细细道来。
根底铺垫
为了文章的顺利进行,必须得进行一个基础知识的铺垫,那就是 Happens-Before 关系。
而 Happens-Before 关系的正式提出,就是 jsr 133 标准:
http://www.cs.umd.edu/~pugh/j…
如果你不晓得 jsr133 是啥,那么能够去这个链接外面看看。
http://ifeve.com/jsr133/
在这外面就有大家耳熟能详的 Happens-Before 关系的正式形容,大家看到的所有的中文版翻译的原文,就是这里:
因为这段话,特地是那六个小黑点前面的话太重要了,失之毫厘谬以千里,所以我不敢轻易依照之前的轻松格调大抵翻译。
于是我决定站在大佬的肩膀上,别离把《深刻了解 Java 虚拟机(第三版)》、《Java 并发编程实战》、《Java 并发编程的艺术》这三本书中对于这部分的定义和形容搬运一下,大家比照着看。
如果对于该规定了然于心,能够跳过本大节。
.png)
走起。
首先是《深刻了解 Java 虚拟机(第三版)》:
- 程序秩序规定(Program Order Rule): 在一个线程内,依照控制流程序,书写在后面的操作后行产生于书写在前面的操作。留神,这里说的是控制流程序而不是程序代码程序,因为要思考分支、循环等构造。
- 管程锁定规定(Monitor Lock Rule): 一个 unlock 操作后行产生于前面对同一个锁的
lock 操作。这里必须强调的是“同一个锁”,而“前面”是指工夫上的先后。- volatile 变量规定(Volatile Variable Rule): 对一个 volatile 变量的写操作后行产生于前面对这个变量的读操作,这里的“前面”同样是指工夫上的先后。
- 线程启动规定(Thread Start Rule):Thread 对象的 start()办法后行产生于此线程的每一个动作。
- 线程终止规定(Thread Termination Rule): 线程中的所有操作都后行产生于对此线程的终止检测,咱们能够通过 Thread::join()办法是否完结、Thread::isAlive()的返回值等伎俩检测线程是否曾经终止执行。
- 线程中断规定(Thread Interruption Rule): 对线程 interrupt()办法的调用后行产生于被中断线程的代码检测到中断事件的产生,能够通过 Thread:interrupted()办法检测到是否有中断产生。
- 对象终结规定(Finalizer Rule): 一个对象的初始化实现(构造函数执行完结)后行产生于它的 finalize()办法的开始。
- 传递性(Transitivity): 如果操作 A 后行产生于操作 B, 操作 B 后行产生于操作 C, 那就能够得出操作 A 后行产生于操作 C 的论断。
接着是《Java 并发编程实战》:
- 程序程序规定:如果程序中操作 A 在操作 B 之前,那么在线程中 A 操作将在 B 操作之前执行。
- 监视器锁规定:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
- volatile 变量规定:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。
- 线程启动规定:在线程上对 Thread.Start 的调用必须在该线程中执行任何操作之前执行。
- 线程完结规定:线程中的任何操作都必须在其余线程检测到该线程曾经完结之前执行,或者从 Thread.join 中胜利返回,或者在调用 Thread.isAlive 时返回 false.
- 中断规定:当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptedException, 或者调用 isInterrupted 和 interrupted).
- 终结器规定:对象的构造函数必须在启动该对象的终结器之前执行实现。
- 传递性:如果操作 A 在操作 B 之前执行,并且操作 B 在操作 C 之前执行,那么操作 A 必须在操作 C 之前执行。
《Java 并发编程的艺术》,在这本书外面作者加了一个限定词“与程序员密切相关的 happens-before 规定如下”:
- 程序程序规定:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
- 监视器锁规定:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
- volatile 变量规定:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
- 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C。
也就是说:线程启动规定、线程完结规定、中断规定、对象终结规定其实对于开发来说是无感的,在这几个规定外面,咱们没有什么能够搞事的空间。
当你把这三本书中对于同一件事件的形容比照着来看的时候,兴许会略微的印象粗浅一点吧。
实质上说的是一回事,只是形容略有不同而已。
另外,我感觉我须要补充一个我感觉十分重要的点,那就是在原文论文中多处呈现的一个十分重要的单词 action:
那么啥是 action?
对于这个略显含糊的定义,论文开篇的第五点提到了具体含意:
In this section we define in more detail some of the informal concepts we have presented.
在本节中,咱们将更具体地定义一些咱们提出的非正式概念。
其中对论文中的七个概念进行了详细描述,别离是:
- Shared variables/Heap memory
- Inter-thread Actions
- Program Order
- Intra-thread semantics
- Synchronization Actions
- Synchronization Order
- Happens-Before and Synchronizes-With Edges
其中,我集体了解,happens-before 中的 action 次要是说上面这个三个概念:
线程间(inter-thread)动作、线程内(intra-thread)动作、同步动作(Synchronization Actions)。
加锁、解锁、对 volatile 变量的读写、启动一个线程以及检测线程是否完结这样的操作,均为同步动作。
而线程间(inter-thread)动作与线程内(intra-thread)动作是相对而言的。比方一个线程对于本地变量的读写,也就是栈上调配的变量的读写,是其余线程无奈感知的,这是线程内动作。而线程间动作比方对于全局变量的读写,也就是堆外面调配的变量,其余线程是能够感知的。
另外,你看 Inter-thread Actions 外面我画下划线的中央,形容其实和同步动作相差无几。我了解,其实线程间动作大多也就是同步动作。
所以你去看一本书,叫做《深刻了解 Java 虚拟机 HotSpot》,这本书外面对于 happens-before 的形容就略微有点不一样了,开篇加的限定条件就是“所有同步动作 …”:
1)所有同步动作(加锁、解锁、读写 volatile 变量、线程启动、线程实现)的代码程序与执行程序统一,同步动作的代码程序也叫作同步程序。
1.1)同步动作中对于同一个 monitor, 解锁产生在加锁后面。
1.2)同一个 volatile 变量写操作产生在读操作后面。
1.3)线程启动操作是该线程的第一个操作,不能有先于它的操作产生。
1.4)当 T2 线程发现 T1 线程曾经实现或者连贯到 T1,T1 的最初一个操作要先于 T2
所有操作。1.5)如果线程 T1 中断线程 T2, 那么 T1 中断点要先于任何确定 T2 被中断的线程的
操作。对变量写入默认值的操作要先于线程的第一个操作;对象初始化实现操作要先
于 finalize()办法的第一个操作。2)如果 a 先于 b 产生,b 先于 c 产生,那么能够确定 a 先于 c 产生。
3)volatile 的写操作先于 volatile 的读操作。
原本,我还想举出《Java 编程思维》外面对于 happens-before 的形容的。
后果,我翻完了书中对于并发的局部,后果它:
没,有,写!
好吧,我想有可能这本神书写于 2004 年 jsr133 公布之前?
后果,它的英文版公布工夫是在 2006 年,也就是作者成心没写的,他只是在 21.11.1 章节外面提到了《Java Concurreny in Practice》:
而《Java Concurreny in Practice》就是咱们后面说的《Java 并发编程实战》。
作为在 Java 界享有如此盛誉的一本书,竟然没有提到 happens-before,稍微有点遗憾。
然而转念一想,这书的江湖位置尽管很高,然而定位其实是入门级的,没提到这块的常识也算是比拟失常。
另外,一个有意思的中央是这样的:
.png)
在《深刻了解 Java 虚拟机(第三版)》外面把 Monitor 翻译为了“管程”,另外两本翻译过去都是“监视器”。
那么“管程”到底是个什么货色呢?
害,原来是一回事啊。
在 Java 外面的 synchronized 就是管程的一种实现。
FutureTask in JDK 8
后面铺垫了这么多,大家应该还没遗记我这篇次要想要分享的货色吧?
那就是“借助同步”这个货色在 FutureTask 外面的利用。
这是 JDK 8 外面的 FutureTask 源码截图,重点关注我框起来的两个局部。
- state 是有 volatile 润饰的。
- outcome 变量前面跟的正文。
着重关注这句正文:
non-volatile, protected by state reads/writes
你想,outcome 外面封装的是一个 FutureTask 的返回,这个返回可能是一个失常的返回,也可能是工作外面的一个异样。
举一个最简略,也是最常见的利用场景:主线通过 submit 形式把工作提交到线程池外面去了,而这个返回值就是 FutureTask:
接下来你会怎么操作?
是不是在主线程外面调用 FutureTask 的 get 办法获取这个工作的返回值?
当初的状况就是:线程池外面的线程对 outcome 进行写入,主线程调用 get 办法对 outcome 进行读取?
这个场景下,咱们的惯例操作是不是得在 outcome 上加一个 volatile,保障可见性?
那么为什么这里没有加 volatile 呢?
你先本人咂摸咂摸。
接下来,要形容的所有货色都是围绕着这个话题开展的。
来,走起。
首先,纵观全局,outcome 变量的写入操作,只有这两个中央:
set 和 setException,而这两个中央的逻辑和原理其实是统一的。所以我就只剖析 set 办法了。
接下来看看 outcome 变量的读取操作,只有这个中央,也就是 get 办法:
须要阐明的是 java.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit)
办法和 get 办法原理统一,也就不做过多解读了。
于是咱们把眼光汇集到了这三个办法上:
get 办法不是调用了 report 办法嘛,咱们把这两个办法合并一下:
这里没故障吧?
接着,咱们其实只关怀 outcome 什么时候返回,其余的对于我来说都是烦扰项,所以咱们把下面的 get 变成伪代码:
当 s 为 NORMAL 的时候,返回 outcome,这伪代码也没故障吧?
上面,咱们再看一下 set 办法:
其中第二行的含意是利用 CAS 操作把状态从 NEW 批改为 COMPLETING 状态,CAS 胜利之后在进入 if 代码段外面。
而后在通过第三行代码,即 outcome=v
之后,状态就批改为了 NORMAL。
其实你看,从 NEW 到 NORMAL,两头这个的 COMPLETING 状态,其实咱们能够说是转瞬即逝。
甚至,如同没啥用似的?
那么为了推理的顺利进行,我决定应用反证法,假如咱们不须要这个 COMPLETING 状态,那么咱们的 set 办法就变成了这个样子:
通过简化之后,这就是最终 set 的伪代码:
于是咱们把 get/set 的伪代码放在一起:
到这里,终于终于所有的铺垫都实现了。
欢送大家来到解密环节。
首先,如果标号为 ④ 的中央,读到的值是 NORMAL,那么阐明标号为 ③ 的中央肯定曾经执行过了。
为什么?
因为 s 是被 volatile 润饰的,依据 happens-before 关系:
volatile 变量规定:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。
所以,咱们能够得出标号为 ③ 的代码先于标号为 ④ 的代码执行。
而又依据程序秩序规定,即:
在一个线程内,依照控制流程序,书写在后面的操作后行产生于书写在前面的操作。留神,这里说的是控制流程序而不是程序代码程序,因为要思考分支、循环等构造。
能够得出 ② happens-before ③ happens-before ④ happens-before ⑤
又依据传递性规定,即:
如果操作 A 后行产生于操作 B, 操作 B 后行产生于操作 C, 那就能够得出操作 A 后行产生于操作 C 的论断。
能够得出 ② happens-before ⑤。
而 ② 就是对 outcome 变量的写入,⑤ 是对 outcome 变量的读取。
尽管被写入,被读取的变量没有加 volatile,然而它通过被 volatile 润饰的 s 变量,借助了 s 变量的 happens-before 关系,实现了同步的操作。
即:写入,先于读取。
这就是“借助同步”。
有没有品到一点点滋味了?
别急,我这反证法呢,还没聊到 COMPLETING 状态呢,咱们持续剖析。
回过头去看 set 办法的伪代码,标号为 ① 的中央我还没说呢。
尽管标号为 ① 的中央和标号为 ③ 的中央都是对 volatile 变量的操作,然而它们之间不是线程平安的,这个点咱们能达成统一吧?
所以,这个中央咱们得用 CAS 来保障线程平安。
于是程序变成了这样:
这样,线程平安的问题被解决了。然而其余的问题也就随之而来了。
第一个问题是程序的含意产生了变动:
从“outcome 赋值实现后,s 才变为 NORMAL”,变成了“s 变成 NORMAL 后,才开始赋值”。
然而,这个问题不在我本文的探讨范畴内,而且最初这个问题也会被解决,所以咱们看另外一个问题,才是我想要探讨的问题。
什么问题呢?
那就是 outcome 的“借助同步”策略失败了。
因为如果咱们通过这样的形式去解决线程平安的问题,把 CAS 操作拆开看,程序就有点像是这样的:
依据 happens-before 关系,咱们只能推断出:
② happens-before ④ happens-before ⑤,和 ③ 没有扯上关系。
所以,咱们不能得出 ③ happens-before ⑤,所以借助不了同步了。
这种时候,如果是咱们碰到了怎么办呢?
很简略嘛,给 outcome 加上 volatile 就行了,哪里还须要这么多奇奇怪怪的推理。
然而 Doug Lea 毕竟是 Doug Lea,加 volatile 多 low 啊,老爷子筹备“借助同步”。
后面咱们剖析了, 这样是能够借助同步的,然而不能保障线程平安:
protected void set(V v) {if (s==NEW) {
outcome = v;
s=NORMAL;
}
}
那么,咱们是不是能够搞成这样:
protected void set(V v) {if (s==NEW) {
s=COMPLETING;
outcome = v;
s=NORMAL;
}
}
COMPLETING 也是对 s 变量的写入呀,这样 outcome 又能“借助同步”了。
用 CAS 优化一下就是这样:
protected void set(V v) {if (compareAndSet(s, NEW, COMPLETING)){
outcome = v;
s=NORMAL;
}
}
引入一个转瞬即逝的 COMPLETING 状态,就能够让 outcome 变量不加 volatile,也能建设起 happens-before 关系,就能达到“借助同步”的目标。
看起来其貌不扬、可有可无的 COMPLETING 状态,居然是一个基于代码优化得出的一个三思而行的产物。
不得不说,老爷子这代码:
真的是“骚”啊,学不来,学不来。
另外,对于 FutureTask 之前我也写过一篇文章,形容的是其另外一个 BUG:
Doug Lea 在 J.U.C 包外面写的 BUG 又被网友发现了。
在这篇文章外面提到了:
老爷子说他“成心这样写的”,这背地是不是还蕴含着“借助同步”的这个背景呢?
不得而知,然而我好像有了一丝“梦幻联动”的感觉。
好了,本次的文章就分享到这里了。
祝贺你,又学到了一个这辈子基本上不会用到的知识点。
再见。
最初说一句
满腹经纶,难免会有纰漏,如果你发现了谬误的中央,能够在留言区提出来,我对其加以批改。
感谢您的浏览,我保持原创,非常欢送并感谢您的关注。