13 Java内存模型
数据竞争
int a=0, b=0;
public void method1() {
int r2 = a;
b = 1;
}
public void method2() {
int r1 = b;
a = 2;
}
上述代码中,定义了两个共享变量 a 和 b,以及两个方法。在单线程分别调用方法一和方法二后,r1 和 r2 的值可能是(1,0) 或者是(0,2)。如果是在多线程环境下,两个方法分别跑在两个线程上,假设 Java 虚拟机在执行了任一方法的第一条赋值语句之后便切换线程,那么最终 r1 和 r2 的结果可能是(0,0)。
除了上述三种情况外,还有可能出现另一种 r1 和 r2 值的情况(1,2)。出现这种情况的原因有三个:即时编译器的重排序,处理器的乱序执行,以及内存系统的重排序。后两种涉及到具体的体系架构,下面我们只分析编译器重排序是怎么回事。
首先声明,即时编译器需要保证程序能够遵守 as-if-serial 属性。也就是说,在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果与顺序执行的结果保持一致。
另外,如果两个操作之间存在数据依赖,那么即时编译器不能调整他们的顺序,否则将会造成程序语义的改变。
int a=0, b=0;
public void method1() {
int r2 = a;
b = 1;
int c = b + 1;
if (r2 == 2) {
int r3 = r2 + 1;
}
}
上述代码中,扩展了方法一。新增代码会先使用变量 b 的值,然后再使用局部变量 r2 的值。此时,编译器有两种选择。
一,在一开始就将 a 加载中某一寄存器中,并且在接下来 b 的赋值操作以及使用 b 的代码中避免使用该寄存器。二:在真正使用 r2 时才将 a 加载至寄存器中。这样的话在使用 b 的时候不用霸占一个寄存器,减少了接触栈空间的情况。
int a=0, b=0;
public void method1() {
for (..) {
int r2 = a;
b = 1;
a = r2 + 1;
}
}
上述代码是把方法一中的代码放入循环体中,并且新增一行代码:使用 r2 并且更新 a。由于对 b 的赋值是循环无关的,即时编译器很有可能将其移出循环之前,而对 r2 的赋值语句还停留在循环之中。
通过上述两段举例分析,我们得出结论:即时编译器的优化可能将原本字段访问的执行顺序打乱。在单线程环境下,由于 as-if-serial 的保证,我们无需担心顺序执行不可能发生情况。例如(r1,r2)的值为(1,2)。
但是在多线程情况下,这种数据竞争的情况是可能发生的。而且,Java 语言规范将其归咎于应用程序没有做出恰当的同步操作。
Java 内存模型与 happens-before 关系
为了避免数据竞争的干扰,Java 5 引入了明确定义的 Java 内存模型。其中有个最重要的概念便是:happens-before 关系。happens-before 关系是用来描述两个操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 是可见的,也就是说 X 操作先于 Y 操作执行。
在同一个线程中,字节码的先后顺序也暗含了 happens-before 关系:控制流靠前的字节码 happens-before 靠后的字节码。但是,如果后者没有观测前者的运行结果,也就是后者没有数据依赖于前者,那么它们的执行顺序就可以能被颠倒。
下面举例线程间的 happens-before 关系:
1:解锁操作 happens-before 对同一把锁枷锁操作。
2:volatile 字段的写操作 happens-before 对同一字段的读操作。
3:线程的启动操作 happens-before 该线程的第一个操作。
4:线程的最后一个操作 happens-before 该线程的终止事件。
5:线程对其他线程的中断操作 happens-before 被中断线程受到中断信号。
6:构造器中的最后一个操作 happens-before 析构器的第一个操作。
happens-before 关系具有传递性。如果 X happens-before Y,Y happens-before Z,那么 X happens-before Y。
文章开头提到 r1 和 r2 的值可能是(1,2),那么如何避免这种结果呢?那就是将 a 或者 b 设置为 volatile 字段。
比如:b 设置为 volatile 字段。假设 r1 可以观测到 b 的赋值结果 1。这样的话,b 的赋值操作要先于 r1 的赋值操作执行。根据 volatile 字段的 happens-before 关系,我们知道 b 的赋值操作 happens-before r1 的赋值操作。然后,再根据同一个线程中字节码暗含 happens-before 关系,以及 happens-before 关系的传递性,可以得出 r2 的赋值操作 happens-before a 的赋值操作。这样的话,就不会出现 r1 和 r2 的值是(1,2)这种情况了。
由此观之,解决数据竞争问题的关键在于构造一个跨线程的 happens-before 关系。
Java 内存模型的底层实现
理解了上述 Java 内存模型的概念后,我们总结下它的底层实现。Java 内存模型是通过内存屏障来进制重排序的。
对于即时编译器,它会对每一个 happens-before 关系向正在编译的目标方法中插入相应的读读,读写,写读以及写写屏障。
这些内存屏障会限制即时编译器的重排序操作。以 volatile 字段的访问为例,所插入的内存屏障不允许 volatile 字段写操作之前的内存访问重排序在其之后,也不允许 volatile 字段读操作之后的内存访问被重排序至其之前。
之后,即时编译器将根据具体的底层体系架构,将这些内存屏障替换成 CPU 指令。
对于 volatile 字段的内存屏障转化而来的指令,可以简单地理解为强制刷新处理器的写缓存。写缓存是处理器用来加速内存存储效率的一项技术。在碰到内存写操作时,处理器并不会等待该指令结束,而是直接开始下一指令,并且依赖于写缓存将更改的数据同步到主内存之中。强指刷新写缓存,将使得当前线程写入 volatile 字段的值(以及写缓存中已有的其他内存修改),同步到主内存之中。
内存写操作同时会无效化其他处理器所持有的,指向同一内存地址的缓存行,因此可以认为其他处理器能够立即见到该 volatile 字段的最新值。
锁,volatile 字段,final 字段
锁操作同样具备 happens-before 关系。具体指:解锁操作 happens-before 对同一把锁加锁操作。实际上,解锁时,Java 虚拟机同样需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。
volatile 字段可以看成一种轻量级,不保证原子性的同步,其性能往往优于锁同步。然而频繁的访问 volatile 字段也会因为不断的强制刷新缓存而严重影响程序的性能。所以,理想情况下对 volatile 字段应该多度少写,并且只有一个线程进行写操作。volatile 字段另一个特性是无法被即时编译器分配到寄存器里。也就是说,volatile 字段的每次访问均需直接存内存中读写。
final 实例字段设计新建对象发布问题。当一个对象包含 final 字段时,其他线程只能读 final 字段。所以,即时编译器会在 final 字段的写操作后插入一个写写屏障,以防某些优化将新建对象的发布重排序至 final 字段的写操作之前。
总结
本文创作灵感来源于 极客时间 郑雨迪老师的《深入拆解 Java 虚拟机》课程,通过课后反思以及借鉴各位学友的发言总结,现整理出自己的知识架构,以便日后温故知新,查漏补缺。
关注本人公众号,第一时间获取最新文章发布,每日更新一篇技术文章。
13 Java内存模型的更多相关文章
- 深入理解Java虚拟机(第三版)-13.Java内存模型与线程
13.Java内存模型与线程 1.Java内存模型 Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到主内存和从内存中取出变量值的底层细节 该变量指的是 实例字 ...
- Java内存模型-jsr133规范介绍
原文地址:http://www.cnblogs.com/aigongsi/archive/2012/04/26/2470296.html; 近期在看<深入理解Java虚拟机:JVM高级特性与最佳 ...
- Java内存模型-jsr133规范介绍(转)
最近在看<深入理解Java虚拟机:JVM高级特性与最佳实践>讲到了线程相关的细节知识,里面讲述了关于java内存模型,也就是jsr 133定义的规范. 系统的看了jsr 133规范的前面几 ...
- 全面理解Java内存模型(JMM)及volatile关键字(转载)
关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...
- JVM学习(3)——总结Java内存模型---转载自http://www.cnblogs.com/kubixuesheng/p/5202556.html
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...
- Java内存模型与共享变量可见性
此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:本文主要参考自<深入理解Java虚拟机(第二版)>和<深入理解Java内存模型> ...
- Java内存模型-volatile的内存语义
一 引言 听说在Java 5之前volatile关键字备受争议,所以本文也不讨论1.5版本之前的volatile.本文主要针对1.5后即JSR-133针对volatile做了强化后的了解. 二 vol ...
- Java内存模型(二)
volatile型变量的特殊规则 volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性” ...
- Java内存模型(一)
主存储器和工作存储器 Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,这些区域包括方法区,堆,虚拟机栈,本地方法栈,程序计数器.方法区存储类信息,常量,字节码等数据 ...
随机推荐
- iOS开发资料
https://github.com/XCGit/awesome-objc-frameworks https://github.com/KevinHM/ios-good-practices-the-l ...
- python内存泄露的诊断(转)
本篇文章非原创,转载自:http://rstevens.iteye.com/blog/828565 . 对于一个用 python 实现的,长期运行的后台服务进程来说,如果内存持续增长,那么很可能是有了 ...
- php之基础深入---类与对象篇
1.类的自动加载: spl_autoload_register()函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载,这样可以避免includ ...
- window.onload中调用函数报错的问题
今天练习js,忽然遇到了一个问题,就是window.onload加载完成后,调用其中的函数会报错, 上一段简单的代码: 报错信息: 报错原因: 当window.onload加载完成后,第一个alert ...
- 模块化Java简介
什么是模块化? 模块化是个一般概念,这一概念也适用于软件开发,可以让软件按模块单独开发,各模块通常都用一个标准化的接口来进行通信.实际上,除了规模大小有区别外,面向对象语言中对象之间的关注点分离与 ...
- vue组件 $children,$refs,$parent的使用
如果项目很大,组件很多,怎么样才能准确的.快速的寻找到我们想要的组件了?? 1)$refs 首先你的给子组件做标记.demo :<firstchild ref="one"&g ...
- python_61_装饰器4
import time def timer(func):#timer(test1) func=test1 def deco(): start_time=time.time() func()#run t ...
- Bootstrap历练实例:简单的可折叠
<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...
- 01_13_JSP编译指令
01_13_JSP编译指令 1. Directive Directive(编译指令)相当于在编译期间的命令 格式: <%@Directive 属性=”属性值”%> 常见的Directive ...
- 【前端_React】React小书
参考书籍:React.js 小书