我们先来看一个反常识的例子。

int a=0, b=0;

public void method1() {
int r2 = a;
b = 1;
} public void method2() {
int r1 = b;
a = 2;
}

这里我定义了两个共享变量 a 和 b,以及两个方法。第一个方法将局部变量 r2 赋值为 a,然后将共享变量 b 赋值为 1。第二个方法将局部变量 r1 赋值为 b,然后将共享变量 a 赋值为 2。请问(r1,r2)的可能值都有哪些?

在单线程环境下,我们可以先调用第一个方法,最终(r1,r2)为(1,0);也可以先调用第二个方法,最终为(0,2)。

在多线程环境下,假设这两个方法分别跑在两个不同的线程之上,如果 Java 虚拟机在执行了任一方法的第一条赋值语句之后便切换线程,那么最终结果将可能出现(0,0)的情况。

除上述三种情况之外,Java 语言规范第 17.4 小节 [1] 还介绍了一种看似不可能的情况(1,2)。

造成这一情况的原因有三个,分别为即时编译器的重排序,处理器的乱序执行,以及内存系统的重排序。由于后两种原因涉及具体的体系架构,我们暂且放到一边。下面我先来讲一下编译器优化的重排序是怎么一回事。

首先需要说明一点,即时编译器(和处理器)需要保证程序能够遵守 as-if-serial 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。

另外,如果两个操作之间存在数据依赖,那么即时编译器(和处理器)不能调整它们的顺序,否则将会造成程序语义的改变。

Java 内存模型与 happens-before 关系

为了让应用程序能够免于数据竞争的干扰,Java 5 引入了明确定义的 Java 内存模型。其中最为重要的一个概念便是 happens-before 关系。happens-before 关系是用来描述两个操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。

在同一个线程中,字节码的先后顺序(program order)也暗含了 happens-before 关系:在程序控制流路径中靠前的字节码 happens-before 靠后的字节码。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者没有观测前者的运行结果,即后者没有数据依赖于前者,那么它们可能会被重排序。

除了线程内的 happens-before 关系之外,Java 内存模型还定义了下述线程间的 happens-before 关系。

  1. 解锁操作 happens-before 之后(这里指时钟顺序先后)对同一把锁的加锁操作。
  2. volatile 字段的写操作 happens-before 之后(这里指时钟顺序先后)对同一字段的读操作。
  3. 线程的启动操作(即 Thread.starts()) happens-before 该线程的第一个操作。
  4. 线程的最后一个操作 happens-before 它的终止事件(即其他线程通过 Thread.isAlive() 或 Thread.join() 判断该线程是否中止)。
  5. 线程对其他线程的中断操作 happens-before 被中断线程所收到的中断事件(即被中断线程的 InterruptedException 异常,或者第三个线程针对被中断线程的 Thread.interrupted 或者 Thread.isInterrupted 调用)。
  6. 构造器中的最后一个操作 happens-before 析构器的第一个操作。

happens-before 关系还具备传递性。如果操作 X happens-before 操作 Y,而操作 Y happens-before 操作 Z,那么操作 X happens-before 操作 Z。

在文章开头的例子中,程序没有定义任何 happens-before 关系,仅拥有默认的线程内 happens-before 关系。也就是 r2 的赋值操作 happens-before b 的赋值操作,r1 的赋值操作 happens-before a 的赋值操作。

拥有 happens-before 关系的两对赋值操作之间没有数据依赖,因此即时编译器、处理器都可能对其进行重排序。举例来说,只要将 b 的赋值操作排在 r2 的赋值操作之前,那么便可以按照赋值 b,赋值 r1,赋值 a,赋值 r2 的顺序得到(1,2)的结果。

那么如何解决这个问题呢?答案是,将 a 或者 b 设置为 volatile 字段。

比如说将 b 设置为 volatile 字段。假设 r1 能够观测到 b 的赋值结果 1。显然,这需要 b 的赋值操作在时钟顺序上先于 r1 的赋值操作。根据 volatile 字段的 happens-before 关系,我们知道 b 的赋值操作 happens-before r1 的赋值操作。

Java 内存模型的底层实现

在理解了 Java 内存模型的概念之后,我们现在来看看它的底层实现。Java 内存模型是通过内存屏障(memory barrier)来禁止重排序的。

对于即时编译器来说,它会针对前面提到的每一个 happens-before 关系,向正在编译的目标方法中插入相应的读读、读写、写读以及写写内存屏障。

这些内存屏障会限制即时编译器的重排序操作。以 volatile 字段访问为例,所插入的内存屏障将不允许 volatile 字段写操作之前的内存访问被重排序至其之后;也将不允许 volatile 字段读操作之后的内存访问被重排序至其之前。

然后,即时编译器将根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。以我们日常接触的 X86_64 架构来说,读读、读写以及写写内存屏障是空操作(no-op),只有写读内存屏障会被替换成具体指令 [2]。

在文章开头的例子中,method1 和 method2 之中的代码均属于先读后写(假设 r1 和 r2 被存储在寄存器之中)。X86_64 架构的处理器并不能将读操作重排序至写操作之后,具体可参考 Intel Software Developer Manual Volumn 3,8.2.3.3 小节。因此,我认为例子中的重排序必然是即时编译器造成的。

总结与实践

今天我主要介绍了 Java 的内存模型。

Java 内存模型通过定义了一系列的 happens-before 操作,让应用程序开发者能够轻易地表达不同线程的操作之间的内存可见性。

在遵守 Java 内存模型的前提下,即时编译器以及底层体系架构能够调整内存访问操作,以达到性能优化的效果。如果开发者没有正确地利用 happens-before 规则,那么将可能导致数据竞争。

Java 内存模型是通过内存屏障来禁止重排序的。对于即时编译器来说,内存屏障将限制它所能做的重排序优化。对于处理器来说,内存屏障会导致缓存的刷新操作。

JVM总结-java内存模型的更多相关文章

  1. 深入理解JVM(6)——Java内存模型和线程

    Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(“即Ja ...

  2. 【JVM】Java内存模型

    原文:多线程之Java内存模型(JMM)(一) 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per S ...

  3. jvm(12)-java内存模型与线程

    [0]README 0.1)本文部分文字描述转自“深入理解jvm”,旨在学习“java内存模型与线程” 的基础知识:   [1]概述 1)并发处理的广泛应用是使得 Amdahl 定律代替摩尔定律称为计 ...

  4. JVM(7) Java内存模型与线程

    衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second,TPS)是最重要的指标之一,它代表着一秒内服务端平均能响应的请求总数,而 TPS 值与程序的并发能力又有非常 ...

  5. 理解JVM之java内存模型

    java虚拟机规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统内存访问差异,以实现让java程序在各种平台都能打到一致的内存访问效果.所以java内存模型的主要目标是定义程序中各 ...

  6. 全面理解Java内存模型

    尊重原创:http://blog.csdn.net/suifeng3051/article/details/52611310 Java内存模型即JavaMemory Model,简称JMM.JMM定义 ...

  7. Java并发编程(四)-- Java内存模型

    Java内存模型 前面讲到了Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见.从抽象的角 ...

  8. 全面理解Java内存模型(转)

    转自:http://blog.csdn.net/suifeng3051/article/details/52611310 Java内存模型即Java Memory Model,简称JMM.JMM定义了 ...

  9. Java 内存模型详解

    概述 Java的内存模型(Java Memory Model )简称JMM.首先应该明白,Java内存模型是一个规范,主要规定了以下两点: 规定了一个线程如何以及何时可以看到其他线程修改过后的共享变量 ...

随机推荐

  1. slice.indices()/collections.Counter笔记

    关于slice.indices() >>> help(slice) Help on class slice in module builtins: class slice(objec ...

  2. Linux shell脚本读取用户输入的参数

    新建一个test.sh文件 #!/bin/sh echo "1 : For Test" echo "2 : For nohup &" whiletrue ...

  3. Linux中chown和chmod的区别和用法

    转载自:http://www.cnblogs.com/EasonJim/p/6525242.html chmod修改第一列内容,chown修改第3.4列内容: chown用法: 用来更改某个目录或文件 ...

  4. RedHat6.5安装单机flume1.6

    版本号: RedHat6.5   JDK1.8   apache-flume-1.6.0 1.apache-flume-1.6.0-bin.tar.gz 下载 官网下载地址:http://archiv ...

  5. centos添加额外测源,解决:No package openvpn available.

    centos添加额外测源,解决:No package openvpn available. ##添加额外的repositories,安装openvpn yum install epel-release ...

  6. 【java】浅谈for循环

    for语法: for(初始化条件; 判断条件(bool型,不可缺省); 条件改变)// 初始化条件,条件改变可以是多条,eg for(x=1,y=1;x<4;x++,y++) { 执行的操作 } ...

  7. NPC问题及证明

    致谢:http://www.docin.com/p-1902790324.html

  8. PHP 如何自定义函数

    PHP 如何自定义函数 使用Function来自定义一个函数:格式如下:function function_name( $data ){ /** * 函数操作 */}注意:函数命名和自定义变量一样.只 ...

  9. git log乱码显示

    1.Linux下UTF8编码 [xusi@pre-srv24 crm2]$ localeLANG=en_US.UTF-8 设置如下: git config --global i18n.commiten ...

  10. bzoj4980: 第一题

    Description 神犇xzyo听说sl很弱,于是出了一题来虐一虐sl.一个长度为2n(可能有前缀0)的非负整数x是good的,当且仅当 存在两个长度为n(可能有前缀0)的非负整数a.b满足a+b ...