您的位置:首页 > 编程语言 > Java开发

Java虚拟机之内存模型

2016-01-26 09:33 399 查看

1 概述

Java内存模型(JMM)是Java虚拟机规范中定义的一种用于屏蔽各种硬件和操作系统的内存访问差异,以实现Java平台到达统一访问内存的效果。Java内存模型的目标是定义程序中各个变量(指实例字段,静态字段,构成数组对象的元素;不包括局部变量,方法参数与异常处理参数这些线程私有变量)的访问规则。即虚拟机中变量存储到内存和从内存取出的规则。JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

2 主内存与工作内存

Java内存模型规定了所有的变量都存储在主内存中;每条线程有自己的工作内存。工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读写)都必须在工作内存中进行,而不能直接读写主内存(即使是volatile修饰的变量也一样)。不同线程之间也不能直接读写对方工作内存中的变量,线程之间的变量值传递需要通过主存完成。工作内存是JMM的抽象概念,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化

这里的主内存和工作内存与堆栈等是两个概念,不要混淆



2.1 内存间的交互

Java内存模型规定了变量在主内存与工作内存见的交互协议,要求以下8种类型的操作都是原子的。

lock 锁定,主要用于主内存的变量,它把一个变量标识为一条线程独占状态。

unlock 解锁,主要用于主内存的变量,lock的反操作,释放这个变量的锁定状态标识。

read 读取,主要用于主内存的变量,将一个变量的值从主内存传输到工作内存(主内存操作,只是读取,未赋值)。

load 载入,主要用于工作内存的变量,将read操作从主内存读取到的变量值放入工作内存的变量副本中(工作内存操作,完成赋值)。

use 使用,主要用于工作内存的变量,将工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

assign 赋值:主要用于工作内存的变量,将一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store 存储:主要用于工作内存的变量,将工作内存中的一个变量的值传送到主存中,(工作内存操作,只是传递,未保存)

write 写入:主要用于主内存的变量,将store操作从工作内存中得到的值写入主内存的变量中(主内存操作,完成写入)。

以上8种类型的操作有以下限制条件:

不允许read和load,store和write操作之一单独出现。

不允许一个线程丢弃它的最近的assign操作,即变量在工作内存改变后必须同步回主内存。

不允许一个线程无原因(没有发生过assign)的把数据从线程工作内存同步到主存去。

一个新的变量只能在主存中诞生,不能在工作内存中直接使用一个未被初始化(load或assign)的变量,也就是说对一个对象实施use或store之前,必须先执行过assign或load。

一个变量在同一时间只允许一个线程对其lock。一个线程可以对一个变量lock多次,且只有unlock相同次数后变量才会被解锁。

若对一个变量执行lock操作,将会请空工作内存中该变量的值,早执行引擎使用这个变量之前,必须使用load或assign操作初始化变量的值。

不允许unlock一个未被lock的变量,且一个线程不能unlock一个其他线程lock的变量。

对一个变量执行unlock之前,必须先吧此变量同步回主内存,即必须先执行完store和write操作。

2.2 volatile型变量

当一个变量定义为volatile之后,其具有两种特性:

虚拟机保证此变量对所有的线程的可见性

禁止指令重排序优化

volatile使用场景有以下两种常见:

运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

变量不需要与其他的状态变量共同参与不变约束。

2.3 两个线程A,B之间的通信

首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

2.4 重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序,才会形成最终的指令序列:

编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

对于编译器重排序(上1),JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止);对于处理器重排序(上2,3),JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)

2.5 处理器重排序与内存屏障指令

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:

屏障类型指令示例说明
LoadLoad BarriersLoad1; LoadLoad; Load2确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore BarriersStore1; StoreStore; Store2确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore BarriersLoad1; LoadStore; Store2确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad BarriersStore1; StoreLoad; Load2确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

2.6 happens-before

happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的happens-before规则如下:

程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。

传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。



一个happens-before规则通常对应于多个编译器重排序规则和处理器重排序规则。对于java程序员来说,happens-before规则简单易懂,它避免程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。

2.7 Java执行顺序规则

以下规则是Java内存模型默认的程序执行规则,即无需任何同步保障手段默认的执行顺序:

程序次序规则(Program Order Rule):在一个线程里,按照源代码的顺序,前面的代码先于后面的代码执行。

管程锁定规则(Monitor Lock Rule):一个unlock操作优先于后面对同一个锁的lock操作。

volatile变量规则:对一个volatile变量写操作先行发生于后面对这个变量的读操作。

线程启动规则:Thread对象的start方法优先于此线程的每一个动作

线程终止规则:Thread对象的所有操作都先行发生于对此线程的终止检测。可以通过Thread.join方法结束,Thread.isAlive方法返回值等手段检测线程已经终止执行。

线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted方法检测到是否有中断发生

对象终结原则:一个对象的初始化完成(构造方法执行结束),优先于它的finalize方法开始。

传递性原则:若A先于B执行,B先于C执行,则A必须先于C执行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: