您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
聊一聊JUC中的“借助同步”
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
聊一聊JUC中的“借助同步”
自猿其说Tech
2021-11-17
IP归属:未知
177080浏览
计算机编程
### 1 前言 在java并发编程领域中,Java内存模型有一个happens-before原则,简称之为HB原则,这个规则定义了java多线程操作的可见行和有序性,防止了编译器重排序对程序运行结果的影响。 **按照官方的说法:** - 当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有 happens-before 关系,则会产生数据竞争问题。 - 要想保证操作 B 的线程看到操作 A 的结果(无论 A 和 B 是否在一个线程),那么在 A 和 B 之间必须满足 HB 原则,如果没有,将有可能导致重排序。 - 当缺少 HB 关系时,就可能出现重排序问题。 ### 2 happens-before有哪些规则? 这个规则在很多经典的书籍中都有对应的讲解,如:**《深入理解Java虚拟机(第三版)》、《Java并发编程实战》、《Java并发编程的艺术》**等。 这里我引用一下《深入理解Java虚拟机(第三版)》中的介绍: 1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作; 1. 锁定规则:在监视器锁上的解锁操作必须在同一个监视器上的加锁操作之前执行。 1. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作; 1. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C; 1. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作; 1. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生; 1. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行; 1. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始; ### 3 “借助同步”在JDK8中的应用 前面的讲解是为这一节讲解“借助同步”做的铺垫工作,在JDK中有这样一个类(FutureTask): ![](//img1.jcloudcs.com/developer.jdcloud.com/f6791cce-f04d-4548-8b74-16baeda3320820211117151332.png) 重点关注一下我图片中框起来的变量: - state是由volatile进行修饰的。 - outcome后边的注释:// non-volatile, protected by state reads/writes 在我们实际的工作之中,我们经常会在一些要求提高性能的时候,优先考虑到线程的复用。但是线程属于稀缺资源,所以就需要使用到池化技术,因此线程池的使用必不可少。 在使用 submit 方法把任务提交到线程池中去之后,它返回给我们的返回值就是一个 FutureTask 的对象: ![](//img1.jcloudcs.com/developer.jdcloud.com/22623aa7-9e10-4137-b8a9-eea88997fe2b20211117151356.png) 在我们需要获取返回值的时候,只需要调用 get 方法,有带超时时间的,还有不带超时时间的: ![](//img1.jcloudcs.com/developer.jdcloud.com/50913012-78aa-4453-a8da-07f1d49ac51220211117151413.png) 当我们在主线程中调用 get 方法对outcome变量进行读取,线程池中的线程对outcome变量进行写入。 对于熟悉并发编程的开发人员来说,可能就需要对outcome变量进行volatile修饰,以此来保证可见行,但是在看源码的时候,我们发现作者并没有这样操作。那是为什么呢??? 在FutureTask的源码中,对outcome变量的写入操作有以下两个地方: ![](//img1.jcloudcs.com/developer.jdcloud.com/e1bf8a4d-f17b-4406-82a5-c58b0c02404420211117151437.png) 在FutureTask的源码中,对outcome变量的读取操作只有一个地方: ![](//img1.jcloudcs.com/developer.jdcloud.com/bdd9f564-3451-4512-ba67-67a5a1f6521520211117151454.png) 我们只需要关心outcome变量的返回,因此可以把get变成这样: ![](//img1.jcloudcs.com/developer.jdcloud.com/35abed92-cc08-4680-af45-1d9870b253b120211117151517.png) 当 s 为 NORMAL 的时候,返回 outcome,否则抛出异常。 接着我们分析一下set方法: ![](//img1.jcloudcs.com/developer.jdcloud.com/de0e00e1-6824-417b-a508-61d58056571f20211117151532.png) 第二行的含义是利用 CAS 操作把状态从 NEW 修改为 COMPLETING 状态,CAS 成功之后在进入 if 代码段里面。 然后再经过第三行代码,即 outcome=v 之后,状态就修改为了 NORMAL。 **从 NEW 到 NORMAL,中间这个的 COMPLETING 状态,其实我们可以说是转瞬即逝。** 感觉好像没有用处似的? 那么为了推理的顺利进行,使用反证法,假设我们没有这个转瞬即逝的 COMPLETING 状态,那么我们的 set 方法就变成了这个样子: ![](//img1.jcloudcs.com/developer.jdcloud.com/0454eb1c-9fad-4404-b6c7-cafc9eaaa23a20211117151549.png) 最终可以将set方法简化为这样子: ![](//img1.jcloudcs.com/developer.jdcloud.com/b848a049-3533-4529-b7e6-a6eabac32e9a20211117151603.png) 为了便于观看,把set和get放在一起: ![](//img1.jcloudcs.com/developer.jdcloud.com/294b288b-1c86-4660-acc2-aa686c53d3f520211117151624.png) 到这里,我们把代码变成了最精简的形式。 首先,如果标号为 ④ 的地方,读到的值是 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 关系,完成了同步的操作。 即:写入,先于读取。 **这就是“借助同步”。** ### 4 总结 在java并发编程中,对于线程的控制异常重要,线程之间的同步和通信也是难点重点,所以作为java开发人员,一定要花出时间来学透它,学透了jdk中的线程池技术,其实对于我们来理解现在常见的分布式微服务架构中,各个微服务之间的调用协作也是有很大的帮助的。 ### 参考文献: 1. 《深入理解Java虚拟机(第三版)》 1. 《Java并发编程实战》 1. 《Java并发编程的艺术》 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:供应链技术部 乔杰
原创文章,需联系作者,授权转载
上一篇:Orika JavaBean映射工具使用
下一篇:京东快递APP的Flutter代码规范实践
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号