Java内存模型
内存模型通俗来理解,就是对物理机的内存包括寄存器高速缓存等构成的计算机运算存储体系的一种抽象。屏蔽底层的复杂性和多样性,抽象出一个一致的对应用程序员友好的一个内存结构。
Java内存模型就是这样的一种抽象,JMM屏蔽了底层硬件和操作系统的多样性,抽象出一个与平台无关的内存模型,并向Java程序员保证一致的内存访问效果。
这也是支持Java跨平台的特性之一。
主内存和工作内存
JMM主要的目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到主存(常说的内存)中以及从主存中读到JVM中的底层细节。
这里的变量是指定义在堆和方法区中的线程共享的变量,而非局部变量。
主存指的就是计算机的RAM,而工作内存,是各个线程私有的工作区,一般是处理器的高速缓存或者是寄存器的抽象定义。线程之间交换数据是通过工作内存和主存的交互完成的,工作内存是线程私有的,其中存储了需要的主内存中的变量的副本,在线程内完成计算后,需要将变量的新值刷新到主内存中才可对其他线程可见,也正因为如此,才会有多线程并发执行时的线程安全问题,这也是定义JMM的原因。
主内存与工作内存的交互
关于变量如何在主存和工作内存之间传递,JMM定义了8中操作,这8种操作JVM保证了他们都是原子操作(但是64位的基本数据类型,在某些平台上可能有例外)。
操作 | 作用 |
---|---|
read | 从主存中把变量传入入工作内存 |
load | 将read进来的变量载入工作内存中的变量副本中 |
use | 把工作内存中的变量的值传给执行引擎,每当JVM遇到一个需要使用到变量值的字节码指令时就会执行此操作 |
assign | 把一个从执行引擎收到的值赋给工作内存的变量 |
store | 把工作内存的变量传递到主存中 |
write | 把store传入主存的变量值写入主存的变量中 |
lock | 把主存中的一个变量标注成一条线程独占的状态,禁止其他线程占有 |
unlock | 把锁定状态的变量释放出来,释放后的变量可被其他线程访问 |
如果要把一个变量从主存复制到工作内存,需要顺序执行read和load,反过来就要顺序执行store和write。是顺序执行而不是连续执行,所以在两个操作之间可以有其他操作的出现。
JMM规定了上面8个操作执行时需要满足以下的规则:
- read和load、store和write要成对出现,不允许一方拒收数据;
- 线程在assign操作后,必须把新值更新到主存中;
- 没有发生assign操作,不允许更新到主存中
- 一个新变量只能在主存中诞生,不能在工作内存操作一个未被初始化的变量;
- 一个对象在同一时刻只能被一个线程lock,但是在同一个线程中可以lock多次,多次lock要对应多次unlock才能unlock对象;
- 如果对一个对象执行lock操作,会清空该线程的工作内存中的变量的副本值,也就是会更新为主存中的最新值;
- 如果一个变量没有被lock,是不可以对其执行unlock操作的,而且不允许去unlock一个被其他lock的变量;
- 对一个变量unlock之前,必须先把工作内存中该变量的副本值刷新到主存中。
volatile变量
volatile是一种轻量级的同步机制(锁),它定义的变量具备两个特性:
- 并发情况下的可见性;
读取一个volatile变量,JVM会保证读取的是主存中的最新值,写入一个volatile变量后,JVM会立即将新值刷新到主存中,也就是对其他线程立即可见
- volatile变量禁止指令重排序优化
下面是JMM针对编译器制定的volatile重排序规则表:
是否能重排序 | 第二个操作 | - | - |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
由上表可知,
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
JMM处理并发中的原子性、可见性和有序性
原子性
- volatile变量的单独的读和写,JMM是保证其原子性的,但是类似v++这种操作,不具有原子性;
- 前面八种操作的lock和unlock操作之间的动作,不会被其他线程截获进入,所以具有原子性;
lock和unlock操作实际上JVM并没有开放给用户使用,是内部使用的操作,JVM提供了更高层次的的字节码指令monitorenter和monitorexit来隐式使用lock和unlock,这两个字节码对应的就是synchronized关键字,因此synchronized块或方法也具有原子性;
可见性
- volatile变量的可见性上面已经说明
- lock操作之前会清除工作内存的副本,从主存读取最新数据,unlock之前会将工作内存的变量刷新到主存,保证了可见性;
- final关键字的字段在构造器中一旦初始化完成,并且构造器没有把this引用传递出去(this引用逃逸),那么其他线程就能立即见到final变量的新值。
实际上看volatile是轻量级的锁机制,他们在处理这些特性时是一样的。
有序性
- volatile变量禁止重排序来确保线程之间的有序执行;
- synchronized则是通过“一个变量在同一时刻只能被一条线程锁定”的规则来确保;
对于volatile来保证有序性可以看下面的示例:
class Example {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //A
flag = true; //B
}
public void reader() {
if (flag) { //C
int i = a * a; //D
}
}
}
上述代码没有使用volatile变量,假设现在两个线程WR共享一个Example实例变量,且线程W执行writer方法,线程R执行reader方法,那么i的值会是多少?1还是0呢 实际上不能确定。
考虑到可能的指令重排序:
B和A重排序了,且在线程W执行指令B之后被中断了,然后线程R执行,线程R发现flag是true了,然后计算i,得到的是0;这种情况是可能中的一种,已经造成了并发的问题。
其他情况不再列举,那么volatile对于上面这种情况,是怎么保证了正常呢?
首先AB不能重排序,然后CD不能重排序,如果W在A之后中断执行权交给R,R读到的flag是false(此时a已经是1了),不会计算i,只会在W继续执行给flag赋值了才会计算。另外volatile的flag变量的赋值具有瞬间的可见性,所以R能成功读取正常执行。
happens-before原则
这个原则是判断数据是否存在竞争、线程是否安全的主要依据。
JSR-133提出了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。
- 线程启动规则: Thread对象的start方法happens-before此线程的每个动作
- 线程终止规则: 线程中的所有操作都happens-before对此线程的终止检测
- 线程中断规则: 对线程interrupt()的调用happens-before被中断线程的代码检测到中断事件的发生
- 对象终结规则: 一个对象的初始化完成happens-before他的finalize()开始
注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。
参考资料
[1] Java并发编程实战
[2] 深入理解Java虚拟机
网友评论