Java内存模型(JMM)详解

在Java JVM系列文章中有朋友问为何要JVM,Java虚拟机不是已经帮咱们处理好了么?一样,学习Java内存模型也有一样的问题,为何要学习Java内存模型。它们的答案是一致的:可以让咱们更好的理解底层原理,写出更高效的代码。面试

就Java内存模型而言,它是深刻了解Java并发编程的先决条件。对于后续多线程中的线程安全、同步异步处理等更是大有裨益。编程

硬件内存架构

在学习Java内存模型以前,先了解一下计算机硬件内存模型。咱们多知道处理器与计算机存储设备运算速度有几个数量级的差异。总不能让处理器老是等待计算机存储设备,这样就没办法显现出处理器的优点。缓存

所以,为了“压榨”处理的性能,达到“高并发”的效果,在处理器和存储设备之间加入了高速缓存(cache)来做为缓冲。安全

JMM

将运算须要使用到的数据复制到缓存中,让运算可以快速进行。当运算完成以后,再将缓存中的结果写入主内存,这样运算器就不用等待主内存的读写操做了。微信

每一个处理器都有本身的高速缓存,同时又共同操做同一块主内存,当多个处理器同时操做主内存时,可能致使数据不一致,所以须要“缓存一致性协议”来保障。好比,MSI、MESI等。多线程

Java内存模型

Java内存模型即Java Memory Model,简称JMM。用来屏蔽掉各类硬件和操做系统的内存访问差别,以实现让Java程序在各平台下都可以达到一致的内存访问效果。架构

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。并发

JMM

JMM与Java内存结构并非同一个层次的内存划分,二者基本没有关系。若是必定要勉强对应,那从变量、主内存、工做内存的定义看,主内存主要对应Java堆中的对象实例数据部分,工做内存则对应虚拟机栈的部分区域。异步

JMM

主内存:主要存储的是Java实例对象,全部线程建立的实例对象都存放在主内存中,无论该实例对象是成员变量仍是方法中的本地变量(也称局部变量),固然也包括了共享的类信息、常量、静态变量。共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。jvm

工做内存:主要存储当前方法的全部本地变量信息(工做内存中存储着主内存中的变量副本拷贝),每一个线程只能访问本身的工做内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在本身的工做内存中建立属于当前线程的本地变量,固然也包括了字节码行号指示器、相关Native方法的信息。因为工做内存是每一个线程的私有数据,线程间没法相互访问工做内存,所以存储在工做内存的数据不存在线程安全问题。

JMM模型与硬件模型直接的对照关系可简化为下图:JMM

内存之间的交互操做

线程的工做内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存中的变量。不一样的线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主内存来完成。

JMM

如上图,本地内存A和B有主内存中共享变量x的副本,初始值都为0。线程A执行以后把x更新为1,存放在本地内存A中。当线程A和线程B须要通讯时,线程A首先会把本地内存中x=1值刷新到主内存中,主内存中的x值变为1。随后,线程B到主内存中去读取更新后的x值,线程B的本地内存的x值也变为了1。

在此交互过程当中,Java内存模型定义了8种操做来完成,虚拟机实现必须保证每一种操做都是原子的、不可再拆分的(double和long类型例外)。

  • lock(锁定):做用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。
  • read(读取):做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用。
  • load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
  • use(使用):做用于工做内存的变量,它把工做内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要使用到变量的值的字节码指令时将会执行这个操做。
  • assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
  • store(存储):做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存中,以便随后的write操做使用。
  • write(写入):做用于主内存的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。

若是须要把一个变量从主内存复制到工做内存,那就要顺序地执行read和load操做,若是要把变量从工做内存同步回主内存,就要顺序地执行store和write操做。注意,Java内存模型只要求上述两个操做必须按顺序执行,而没有保证是连续执行。也就是说read与load之间、store与write之间是可插入其余指令的,如对主内存中的变量a、b进行访问时,一种可能出现顺序是read a、read b、load b、load a。除此以外,Java内存模型还规定了在执行上述8中基本操做时必须知足以下规则。

  • 不容许read和load、store和write操做之一单独出现,即不容许一个变量从主内存读取了但工做内存不接受,或者从工做内存发起回写了但主内存不接受的状况出现。
  • 不容许一个线程丢弃它的最近的assign操做,即变量在工做内存中改变了以后必须把该变化同步回主内存。
  • 不容许一个线程无缘由地(没有发生过任何assign操做)把数据从线程的工做内存同步回主内存。
  • 一个新的变量只能在主内存中“诞生”,不容许在工做内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操做以前,必须先执行过了assign和load操做。
  • 一个变量在同一时刻只容许一条线程对其进行lock操做,但lock操做能够被同一条线程重复执行屡次,屡次执行lock后,只有执行相同次数的unlock操做,变量才会被解锁。
  • 若是对一个变量执行lock操做,那将会清空工做内存中此变量的值,在执行引擎使用这个变量前,须要从新执行load或assign操做初始化变量的值。
  • 若是一个变量事先没有被lock操做锁定,那就不容许对它执行unlock操做,也不容许去unlock一个被其余线程锁定住的变量。
  • 对一个变量执行unlock操做以前,必须先把此变量同步回主内存中(执行store、write操做)。

long和double型变量的特殊规则

Java内存模型要求lock,unlock,read,load,assign,use,store,write这8个操做都具备原子性,但对于64位的数据类型(long或double),在模型中定义了一条相对宽松的规定,容许虚拟机将没有被volatile修饰的64位数据的读写操做划分为两次32位的操做来进行,即容许虚拟机实现选择能够不保证64位数据类型的load,store,read,write这4个操做的原子性,即long和double的非原子性协定。

若是多线程的状况下double或long类型并未声明为volatile,可能会出现“半个变量”的数值,也就是既非原值,也非修改后的值。

虽然Java规范容许上面的实现,但商用虚拟机中基本都采用了原子性的操做,所以在平常使用中几乎不会出现读取到“半个变量”的状况。

小结

本节课重点介绍了Java内存模型以及内存交互的步骤和操做。下篇文章将重点介绍Java内存模型涉及的几个特征和原则。欢迎关注微信公众号“程序新视界”,第一时间得到最新文章的更新。

原文连接:《Java内存模型(JMM)详解

《面试官》系列文章:


程序新视界:精彩和成长都不容错过

程序新视界-微信公众号

相关文章
相关标签/搜索