【JAVA】Java 内存模型中的 happen-before
前言
Java 语言在设计之初就引入了线程的概念,以充分利用现代处理器的计算能力,这既带来了强大、灵活的多线程机制,也带来了线程安全等令人混淆的问题,而 Java 内存模型(Java Memory Model,JMM)为我们提供了一个在纷乱之中达成一致的指导准则。
本篇博文的重点是,Java 内存模型中的 happen-before 是什么?
概述
Happen-before 关系,是 Java 内存模型中保证多线程操作可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义。
它的具体表现形式,包括但远不止是我们直觉中的 synchronized、volatile、lock 操作顺序等方面,例如:
- 线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
- 对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
- 对于一个锁的解锁操作,保证 happen-before 加锁操作。
- 对象构建完成,保证 happen-before 于 finalizer 的开始动作。
- 甚至是类似线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。
这些 happen-before 关系是存在着传递性的,如果满足 a happen-before b 和 b happen-before c,那么 a happen-before c 也成立。
前面我一直用 happen-before,而不是简单说前后,是因为它不仅仅是对执行时间的保证,也包括对内存读、写操作顺序的保证。仅仅是时钟顺序上的先后,并不能保证线程交互的可见性。
正文
为什么需要 JMM,它试图解决什么问题?
Java 是最早尝试提供内存模型的语言,这是简化多线程编程、保证程序可移植性的一个飞跃。早期类似 C、C++ 等语言,并不存在内存模型的概念(C++ 11 中也引入了标准内存模型),其行为依赖于处理器本身的内存一致性模型,但不同的处理器可能差异很大,所以一段 C++ 程序在处理器 A 上运行正常,并不能保证其在处理器 B 上也是一致的。
即使如此,最初的 Java 语言规范仍然是存在着缺陷的,当时的目标是,希望 Java 程序可以充分利用现代硬件的计算能力,同时保持“书写一次,到处执行”的能力。
但是,显然问题的复杂度被低估了,随着 Java 被运行在越来越多的平台上,人们发现,过于泛泛的内存模型定义,存在很多模棱两可之处,对 synchronized 或 volatile 等,类似指令重排序时的行为,并没有提供清晰规范。这里说的指令重排序,既可以是编译器优化行为,也可能是源自于现代处理器的乱序执行等。
换句话说:
- 既不能保证一些多线程程序的正确性,例如最著名的就是双检锁(Double-Checked Locking,DCL)的失效问题,双检锁可能导致未完整初始化的对象被访问,理论上这叫并发编程中的安全发布(Safe Publication)失败。
- 也不能保证同一段程序在不同的处理器架构上表现一致,例如有的处理器支持缓存一致性,有的不支持,各自都有自己的内存排序模型。
所以,Java 迫切需要一个完善的 JMM,能够让普通 Java 开发者和编译器、JVM 工程师,能够清晰地达成共识。换句话说,可以相对简单并准确地判断出,多线程程序什么样的执行序列是符合规范的。
所以:
- 对于编译器、JVM 开发者,关注点可能是如何使用类似内存屏障(Memory-Barrier)之类技术,保证执行结果符合 JMM 的推断。
- 对于 Java 应用开发者,则可能更加关注 volatile、synchronized 等语义,如何利用类似 happen-before 的规则,写出可靠的多线程应用,而不是利用一些“秘籍”去糊弄编译器、JVM。
我画了一个简单的角色层次图,不同工程师分工合作,其实所处的层面是有区别的。JMM 为 Java 工程师隔离了不同处理器内存排序的区别,这也是为什么我通常不建议过早深入处理器体系结构,某种意义上来说,这样本就违背了 JMM 的初衷。
JMM 是怎么解决可见性等问题的呢?
在这里有必要简要介绍一下典型的问题场景。
在 【JAVA】JVM 内存区域的划分 里介绍了 JVM 内部的运行时数据区,但是真正程序执行,实际是要跑在具体的处理器内核上。你可以简单理解为,把本地变量等数据从内存加载到缓存、寄存器,然后运算结束写回主内存。你可以从下面示意图,看这两种模型的对应。
看上去很美好,但是当多线程共享变量时,情况就复杂了。试想,如果处理器对某个共享变量进行了修改,可能只是体现在该内核的缓存里,这是个本地状态,而运行在其他内核上的线程,可能还是加载的旧状态,这很可能导致一致性的问题。从理论上来说,多线程共享引入了复杂的数据依赖性,不管编译器、处理器怎么做重排序,都必须尊重数据依赖性的要求,否则就打破了正确性!这就是 JMM 所要解决的问题。
JMM 内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种 happen-before 规则。与此同时,更多复杂度在于,需要尽量确保各种编译器、各种体系结构的处理器,都能够提供一致的行为。
我以 volatile 为例,看看如何利用内存屏障实现 JMM 定义的可见性?
对于一个 volatile 变量:
- 对该变量的写操作之后,编译器会插入一个写屏障。
- 对该变量的读操作之前,编译器会插入一个读屏障。
内存屏障能够在类似变量读、写操作之后,保证其他线程对 volatile 变量的修改对当前线程可见,或者本地修改对其他线程提供可见性。换句话说,线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。
如果你对更多内存屏障的细节感兴趣,或者想了解不同体系结构的处理器模型,建议参考 JSR-133 相关文档,我个人认为这些都是和特定硬件相关的,内存屏障之类只是实现 JMM 规范的技术手段,并不是规范的要求。
从应用开发者的角度,JMM 提供的可见性,体现在类似 volatile 上,具体行为是什么样呢?
我这里循序渐进的举两个例子。
首先,请看下面的代码片段,希望达到的效果是,当 condition 被赋值为 false 时,线程 A 能够从循环中退出。
// Thread A
while (condition) {
}// Thread B
condition = false;
复制代码
这里就需要 condition 被定义为 volatile 变量,不然其数值变化,往往并不能被线程 A 感知,进而无法退出。当然,也可以在 while 中,添加能够直接或间接起到类似效果的代码。
第二,我想举 Brian Goetz 提供的一个经典用例,使用 volatile 作为守卫对象,实现某种程度上轻量级的同步,请看代码片段:
Map configOptions;
char[] configText;
volatile boolean initialized = false;// Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;// Thread B
while (!initialized)sleep();
// use configOptions
复制代码
JSR-133 重新定义的 JMM 模型,能够保证线程 B 获取的 configOptions 是更新后的数值。
也就是说 volatile 变量的可见性发生了增强,能够起到守护其上下文的作用。线程 A 对 volatile 变量的赋值,会强制将该变量自己和当时其他变量的状态都刷出缓存,为线程 B 提供可见性。当然,这也是以一定的性能开销作为代价的,但毕竟带来了更加简单的多线程行为。
我们经常会说 volatile 比 synchronized 之类更加轻量,但轻量也仅仅是相对的,volatile 的读、写仍然要比普通的读写要开销更大,所以如果你是在性能高度敏感的场景,除非你确定需要它的语义,不然慎用。
后记
以上就是 【JAVA】Java 内存模型中的 happen-before 的所有内容了;
从 happen-before 关系开始,帮你理解了什么是 Java 内存模型。为了更方便理解,我作了简化,从不同工程师的角色划分等角度,阐述了问题的由来,以及 JMM 是如何通过类似内存屏障等技术实现的。最后,我以 volatile 为例,分析了可见性在多线程场景中的典型用例。。
【JAVA】Java 内存模型中的 happen-before相关推荐
- java happen-before_java 内存模型中的happen-before 是什么?
happen-before 关系,是Java内存模型中保证多线程可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义. 它的具体表现形式,包括但远不止测试我们直觉中的 synchroniz ...
- 全网最硬核 Java 新内存模型解析与实验 - 4. Java 新内存访问方式与实验
个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...
- 什么是Java内存模型中的happens-before
Java内存模型JMM Java内存模型(即Java Memory Model , 简称JMM),本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序个各个变量(包括实 ...
- java jvm内存模型_Java(JVM)内存模型– Java中的内存管理
java jvm内存模型 Understanding JVM Memory Model, Java Memory Management are very important if you want t ...
- Java内存模型中的三个代
http://developer.51cto.com/art/200909/153154.htm 本文介绍Java内存模型中的三个代:年轻代,终身代以及永久代.文中图示了各个代的默认排列.本文适用于J ...
- Java之内存模型的基础、重排序、顺序一致性、volatile、锁、final
为什么80%的码农都做不了架构师?>>> 深入理解Java内存模型(一)--基础 深入理解Java内存模型(二)--重排序 深入理解Java内存模型(三)--顺序一致性 深入理 ...
- 软件测试学java,软件测试学习Java的内存模型
原标题:软件测试学习Java的内存模型 软件测试学习Java的内存模型 主内存与工作内存 定义程序中各个变量的访问规则. 规定JVM必须遵循的最小保证. 定义操作的偏序关系.Happens-Befor ...
- 【Android 内存优化】Java 内存模型 ( Java 虚拟机内存模型 | 线程私有区 | 共享数据区 | 内存回收算法 | 引用计数 | 可达性分析 )
文章目录 一. Java 虚拟机内存模型 二. 程序计数器 ( 线程私有区 ) 三. 虚拟机栈 ( 线程私有区 ) 四. 本地方法栈 ( 线程私有区 ) 五. 方法区 ( 共享数据区 ) 1. 方法区 ...
- java——JMM内存模型
目录 简介 三大特性 volatile关键字 简介 JMM内存模型,也称为Java多线程内存模型.它和cpu缓存模型类似,是基于cpu缓存模型来建立. cpu缓存模型: java内存模型: 当Java ...
最新文章
- MyBatis复习笔记5:MyBatis代码生成器
- 用Node.js 写web框架(番外)
- linux 查看lv格式,linux lv pv lv 命令
- r语言 服务器网页版ide RStudio Server 简介
- ansible模块介绍
- linux两文件对比,linux对比两个文件的差异
- Major GC 是清理老年代。 Full GC 是清理整个堆空间—包括年轻代和老年代。
- 获取Windows 10(1)
- MySQL 每周总结(4周)
- html与css知识点集合
- 2019配电安规电子版_2018年配电安规.docx
- 缩减Centos7xfs磁盘空间
- 最简单的基于Flash的流媒体示例 网页播放器(HTTP,RTMP,HLS)
- scratch课程设计
- SDS很好,但处理器需“减负”!
- php时间戳与date格式转换
- (翻译)斑马纹模式(Alternating Row Colors)
- JASMINER X4为什么能异军突起?
- 智能安防不谢幕 探讨重点领域发展趋势
- java 文件大小计算
热门文章
- 2020年4月14日
- Android 判断app是否正在播放音乐
- php jsp显示数据排序,JSP_SQL数据库开发中的一些精典代码,1.按姓氏笔画排序: select * From T - phpStudy...
- Part1---3.数据库编程与完整性
- js屏蔽键盘esc键
- lol微信登录服务器,lol开放微信登录功能 lol微信怎么登陆
- PM小课堂 项目管理中的甘特图妙用
- 手游代理平台官方最全解释
- 计算机上下打字怎么打,电脑打字符号怎么打底下的一横
- Raft 实现日志复制同步