Java内存模型

在物理机中,为了加快处理器与内存交互速度,在处理器中引入了高速缓存,将运算需要使用到的数据复制到缓存中,运算结束后再从缓存同步到内存中。这样在多线程处理数据时,可能会导致不同处理器中缓存数据不一致,这个时候引入了缓存一致性协议,这样能保证一个处理器修改了数据,其他处理也能看到最新的数据。

在Java虚拟机规范中定义了一种Java内存模型(Java Memory Model, JMM)来屏蔽各种硬件和操作系统的内存访问差异,让Java程序在不同平台都能达到一致的内存访问效果。(JDK1.5之后内存模型才相对完善)

在Java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java内存模型规定了共享变量存储在主内存中,每条线程都有自己的工作内存(线程本地内存),工作内存中存储被该线程使用到的变量的主内存的副本(不是整个对象的内存复制,对象的引用、线程访问的字段)。如图:

在该图中,如果线程A修改了本地变量,然后把本地变量更新到主内存中,然后线程B把主内存中变量同步到本地内存中。
内存图:

这样就引出两个问题:

  1. 线程A修改了本地变量,怎么同步到主内存中
  2. 线程B怎么同步主内存数据到本地内存中

内存交互

对于工作内存与主内存之间交互协议,Java内存模型定义了8种操作来完成,虚拟机实现时,必须保证每种操作都是原子(对于double、long,允许 load、store、read、write操作在某些平台上例外)。

- 作用域 用途
lock(锁定) 主内存变量 把一个变量标识为一条线程独占
unlock(解锁) 主内存的变量 把一个处于锁定状态的变量释放,释放后的变量才可以被别的变量锁定
read(读取) 主内存的变量 把一个变量值从主内存中传输到线程的工作内存中,以便后续load使用
load(载入) 工作内存变量 把read操作从主内存中得到的变量放入工作内存变量副本中
use(使用) 工作内存变量 把工作内存中变量传递到执行引擎,当虚拟机遇到需要使用该变量值的字节码指令时,会执行该操作
assign(赋值) 工作内存变量 把执行引擎接收到的值,赋值给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时执行该操作
store(存储) 工作内存变量 把工作内存中的变量值传送到主内存中,以便随后write使用
write(写入) 主内存变量 把store操作从工作内存中获取的变量写入到主内存中

Java中定义了改8种操作,如果把主内存中数据复制到工作内存中,必须顺序执行read–>load(并不一定连续,中间可插入其他指令),同时还规定了必须满足以下规则。

  • read、load、store、write不能单独出现,不能read后没有load,store后没有write。
  • 不能丢弃assign
  • 不能没有assign操作,之后同步工作内存到主内存
  • 新变量只能在主内存中出现,不允许在工作内存中使用未初始化变量(未执行load、assign),也就是必须先load或者assign、才能操作use或者store
  • 一个变量同一时间只能一个线程进行lock,lock可以被同一线程多次执行,多次执行lock后需要多次执行unlock才解锁
  • 未lock不能unlock,不允许unlock其他线程锁定的变量
  • 对变量unlock之前,必须先把改变量同步到主内存中。

对于long和double(64位数据类型),虚拟机允许没有没volatile修饰的变量读写操作(load、store、read、write)拆分为2次32位操作来进行(非原子性协议)。虽然Java内存模型允许不把long和double的读写实现为原子操作,但允许虚拟机把改操作实现为原子操作,当前个平台几乎都是作为原子操作实现。

原子性、可见性、有序性

Java内存模型在并发过程中主要围绕原子性、可见性、有序性:

  • 原子性:由内存模型直接保证原子性,所以常用的读写是具备原子性,如果需要大范围原子性,需要使用到lock、unlock,用户无法直接使用lock和unlock,但是提供了字节码指令monitorenter、monitorexit,隐式使用lock、unlock,synchronized编译后就是使用的该字节码指令。
  • 可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存中,在变量读取前,从主内存中刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。Java中除了volatile外还有synchronized和final实现可见性,synchronized通过unlock前必须同步到主内存中实现。final实现的关键字在构造器中一旦初始化完成,并且构造器没有把this应用传递出去,那么其他线程中就能看见final字段的值(具体可搜索final可见性、JSR-133final增强)。
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。但是在本线程内观察,所有操作都是有序的。volatile和synchronized两个关键字保证了线程之间操作的有序性。

hanpens-before

编译器和处理器为了优化程序性能,可能会对指令顺序重排序,在单线程下,重排序并不会影响运行结果,但是再多线程下,重排序会导致各种问题出现。为了避免这种问题,JMM使用了hanpens-before这一规则。
happens-before规则对于会改变查询执行结果的重排序(指单线程和正确同步的多线程),禁止编译器和处理器重排序。对于不会改变执行结果的重排序,并不禁止。
其他概念参考:http://blog.csdn.net/ns_code/article/details/17348313

内存屏障

可参考:http://gee.cs.oswego.edu/dl/jmm/cookbook.html

并不是非常理解这一部分:
相关资料:http://www.cs.umd.edu/~pugh/java/memoryModel/

参考:http://www.infoq.com/cn/articles/java-memory-model-1
参考:《深入Java虚拟机》