数据竞争

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内存模型的更多相关文章

  1. 深入理解Java虚拟机(第三版)-13.Java内存模型与线程

    13.Java内存模型与线程 1.Java内存模型 Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到主内存和从内存中取出变量值的底层细节 该变量指的是 实例字 ...

  2. Java内存模型-jsr133规范介绍

    原文地址:http://www.cnblogs.com/aigongsi/archive/2012/04/26/2470296.html; 近期在看<深入理解Java虚拟机:JVM高级特性与最佳 ...

  3. Java内存模型-jsr133规范介绍(转)

    最近在看<深入理解Java虚拟机:JVM高级特性与最佳实践>讲到了线程相关的细节知识,里面讲述了关于java内存模型,也就是jsr 133定义的规范. 系统的看了jsr 133规范的前面几 ...

  4. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  5. JVM学习(3)——总结Java内存模型---转载自http://www.cnblogs.com/kubixuesheng/p/5202556.html

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...

  6. Java内存模型与共享变量可见性

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:本文主要参考自<深入理解Java虚拟机(第二版)>和<深入理解Java内存模型> ...

  7. Java内存模型-volatile的内存语义

    一 引言 听说在Java 5之前volatile关键字备受争议,所以本文也不讨论1.5版本之前的volatile.本文主要针对1.5后即JSR-133针对volatile做了强化后的了解. 二 vol ...

  8. Java内存模型(二)

    volatile型变量的特殊规则 volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性” ...

  9. Java内存模型(一)

    主存储器和工作存储器 Java虚拟机在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域,这些区域包括方法区,堆,虚拟机栈,本地方法栈,程序计数器.方法区存储类信息,常量,字节码等数据 ...

随机推荐

  1. [转]C#中StreamReader读取中文出现乱码

    摘自:C#中StreamReader读取中文出现乱码 原因是自Windows 2000之后的操作系统在文件处理时默认编码采用Unicode所以.NET文件的默认编码也是Unicode.除非另外指定,S ...

  2. Android商城开发系列(十四)—— 设置监听RecyclerView的位置

    在前面的博客中有讲到过点击一个图片按钮控制RecyclerView的滚动到顶部位置的效果,但是那个图片按钮一直处在一个显示的状态,今天我们来改造一下那个地方,我们要实现的效果是:一开始打开的时候看不到 ...

  3. Coursera 算法二 week2 Seam Carving

    这周作业设计到的算法是有向无环图的最短路径算法,只需要按照顶点的拓扑顺序去放松顶点即可.而在这个题目中拓扑顺序就是按照行的顺序或列的顺序. 用到的数据结构为一个二维数组picture同来存储每个像素的 ...

  4. StackOverflow之旅<2>------{HashMap和Hashtable的区别}

    问题 在Java中HashMap和Hashtable的区别? 哪一个对于多线程应用程序更好? 回答 Hashtable是同步的,加了synchronized锁,而HashMap不是.没有加synchr ...

  5. codeforces 599D Spongebob and Squares

    很容易得到n × m的方块数是 然后就是个求和的问题了,枚举两者中小的那个n ≤ m. 然后就是转化成a*m + c = x了.a,m≥0,x ≥ c.最坏是n^3 ≤ x,至于中间会不会爆,测下1e ...

  6. python_7_while

    count=0 while True: print('count:',count) count+=1 # count=count+1 if count==500: break#结束整个循环

  7. javascrit中“字符串为什么可以调用成员”

    <script> var title = "this is title"; console.log(title.substr(0,5));   //字符串为什么可以调用 ...

  8. 微服务SpringCloud+Docker入门到高级实战(目录)

    第一章 课程介绍和学习路线 1.微服务架构SpringCloud课程介绍 简介:课程介绍和课程大纲讲解,讲课风格和重点内容理解技巧2.技术选型和学后水平 简介:课程所需基础和技术选型讲解,学完课程可以 ...

  9. js中的||、&&与!用法

    &&和||在JQuery源代码内尤为使用广泛,由网上找了些例子作为参考,对其用法研究了一下: 1. && function a(){ alert("a&quo ...

  10. mysql 索引的统计

    查看一个库里面没有使用过的索引select object_type,object_schema,object_name,index_name,count_star,count_read,COUNT_F ...