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程序的过程中会把它管理的内存划分为若干个不同的数据区域,这些区域包括方法区,堆,虚拟机栈,本地方法栈,程序计数器.方法区存储类信息,常量,字节码等数据 ...
随机推荐
- LeetCode Count and Say 数数字
class Solution { public: string countAndSay(int n) { ) "; "; int i,t,count; char c='*'; ;i ...
- python_16_自己建立模块
import python_5_password
- Python 之私有属性
概要 在基类的定义中,如果有些属性或者方法,我们希望隐藏它,从而不被子类继承,或者使其不被实例直接访问到,这时候可以用到私有属性的命名方法.尽管类的所有属性和方法在某种意义上说都是"暴露的& ...
- Python02 变量
变量 因为Python是弱变量类型编程语言,所以变量赋值不需要类型声明. 每个变量在内存中创建,都包括变量的标识,名称和数据这些信息. 每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建. 变量 ...
- PAT (Basic Level) Practise (中文)- 1001. 害死人不偿命的(3n+1)猜想 (15)
http://www.patest.cn/contests/pat-b-practise/1001 卡拉兹(Callatz)猜想: 对任何一个自然数n,如果它是偶数,那么把它砍掉一半:如果它是奇数,那 ...
- Java代码工具箱之解析单行单列简单Excel
1. 使用开源工具 jxl.jar 2. 功能:解析常规Excel.xls格式测试可行,xlsx未测试.Excel测试格式为常规类似table这种简单布局文件.第一行为标题,后面行为内容.代码 可正确 ...
- AngularJS 历经实例
<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...
- Linux运维常用命令详解
1.ls 文件属性: -:普通文件 d:目录文件 b:块设备 c:字符设备文件 l:符号连接文件 p:命令管道 s:套接字文件 文件权限: 9位数字,每3位一组 文件硬链接次数 文 ...
- mysql基础,修改数据表
- git bash 学习2 --更改url 重置密钥 Permission denied (publickey)问题
在今天的上传过程中,我意外地遇到了一个问题,,每一次push都会出现 $ git push origin master Permission denied (publickey). fatal: Co ...