一.Java内存模型概述
Java内存模型即Java Memory Model,简称JMM。
Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。
它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。
Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile
和synchronized
的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
对于程序员来说,JMM是一套协议,这套协议描绘了一个模型,通过认知这个模型,按照协议的规定,程序员编写出的代码可以由JMM来保障(在多线程环境下)如期运行而不用在意各种硬件和操作系统的内存访问差异.
1.线程通信与同步
在并发编程领域,有两个关键问题:线程之间的通信
和同步
。
1).线程之间的通信
线程的通信是指线程之间以何种机制来交换信息
。
在命令式编程中,线程之间的通信机制有两种共享内存和消息传递。
共享内存并发模型的通信
- 定义
线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式
进行通信 - 典型的共享内存通信方式
通过共享对象进行通信。
- 定义
消息传递并发模型的通信
- 定义
线程之间没有公共状态,线程之间必须通过明确的发送消息来显式
进行通信 - 典型的消息传递方式
wait()和notify()。
- 定义
2).线程之间的同步
线程的同步是指程序用于控制不同线程之间操作发生相对顺序
的机制。
共享内存并发模型的同步
显式进行
:程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。消息传递并发模型的同步
隐式进行
:由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
并发模型 | 通信 | 同步 |
---|---|---|
共享内存 | 隐式 | 显式 |
消息传递 | 显式 | 隐式 |
Java的并发采用的是共享内存
模型
Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
另外Java线程之间的同步需要程序员显式指定互斥执行.
2.变量与内存
1).变量分类(共享与不共享)
在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
- 共享(受内存模型影响):所有实例域、静态域和数组元素
- 不共享(不受内存模型影响):局部变量,方法参数,异常处理器参数
后文内存模型中说到的变量一般指共享变量.
2).主内存与本地内存(工作内存)
JMM决定一个线程对共享变量的写入何时对另一个线程可见。
线程和内存之间的关系:
线程之间所有的共享变量存储在主内存
(main memory)中,
每个线程都有一个私有
的本地内存
(local memory),本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。工作流程
- 创建本地内存:
JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(即本地内存) - 读取主内存:
变量从主内存拷贝到本地内存空间 - 操作本地内存:
对变量进行操作 - 回写主内存:
操作完成后再将变量写回主内存
- 创建本地内存:
- 注意:
- 主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行
- 在JMM语义下,主内存和本地内存存储的都是共享变量(原本和副本的区别)
非共享变量(如局部变量)的存储位置不在JMM语义下作讨论 - 有些人喜欢把主内存和本地内存跟JVM内存空间的java堆和java虚拟机栈作对比,但其实并不严谨,主内存和本地内存本来就是抽象概念,并不真实对应,所以两者基本上没有关系
不过理解上,有一部分人认为本地内存对应java虚拟机栈,程序计数器等线程私有的空间,另一部分人(如<深入理解java虚拟机>)则认为本地内存对应虚拟机栈的部分区域,我更偏向于后者的观点,因为主内存和本地内存本来就是JMM语义下的产物,其实现一定是线程私有的,故应当在java虚拟机栈中.但JMM语义外的非共享变量(如局部变量)虽然也是在虚拟机栈中,却不一定属于本地内存.
另外从更低层次说,主内存应该对应于物理硬件的内存,而本地内存(工作内存)则应该存储于寄存器和高速缓存中.
内存类型 | 访问权限 | 内容 | 功能 |
---|---|---|---|
主内存 | 线程共享 | 共享变量 | 储存所有的共享变量 |
本地内存(工作内存) | 线程私有 | 共享变量的副本 | 完成线程对变量的操作(读写) |
3).共享内存并发模型的通信
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存(隐式通信)。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
二.Java内存模型的承诺
JMM能在多线程以及重排序的环境下给程序员承诺原子性,可见性和有序性,并通过Java内存模型的各种保障机制来实现
1.重排序
重排序是程序员需要Java内存模型给予承诺的重要原因
1).重排序分类
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
编译器优化
的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
。指令级并行
的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
。内存系统
的重排序
由于处理器使用缓存
和读/写缓冲区
,这使得加载和存储操作看上去可能是在乱序执行。
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
上述的1属于编译器重排序,2和3属于处理器重排序。
这些重排序都可能会导致多线程程序出现内存可见性问题。
- 对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
- 对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的
内存屏障
(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
重排序类型 | 顺序 | 重排序位置 | 重排序原理 | 禁用方法 |
---|---|---|---|---|
编译器优化重排序 | 1 | 编译器 | 编译时新安排语句的执行顺序 | 禁止特定类型的编译器重排序 |
指令级并行重排序 | 2 | 处理器 | 指令级并行技术将多条指令重叠执行,处理器可以改变语句对应机器指令的执行顺序。 | 通过插入特定类型的内存屏障指令来禁止特定类型的处理器重排序 |
内存系统重排序 | 3 | 处理器 | 处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行 | 通过插入特定类型的内存屏障指令来禁止特定类型的处理器重排序 |
2).编译器指令重排
先来看一个小例子
线程A | 线程B |
---|---|
x=a(步骤1) | y=b(步骤3) |
b=1(步骤2) | a=2(步骤4) |
假定x,y,a,b一开始都是0,(x,y)最后有哪些可能的结果?
执行步骤顺序 | (x,y)结果 |
---|---|
1.2.3.4(A,B顺序执行) | (0,1) |
3.4.1.2(B,A顺序执行) | (2,0) |
1,3,2,4和1,3,4,2(A,B交叉执行) | (0,0) |
3,1,2,4和3,1,4,2(B,A交叉执行) | (0,0) |
以上6种执行步骤顺序都是多线程下不加锁的可能情况
不管出现哪种结果,执行步骤都遵循1在2前,3在4前
编译器在不改变单线程程序语义(as-if-serial
)的前提下,可以重新安排语句的执行顺序
。
但实际上,在不改变各自的线程程序语义的前提下,编译器就可以重新安排语句的执行顺序。(比方说在线程A中,编译器会认为步骤1和步骤2的执行无数据依赖性
,1.2和2.1不影响程序语义,于是对其重排序)
所以,真正可能的执行步骤顺序有
种,比方说4.2.3.1,其(x,y)结果为(2,1)
说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
as-if-serial
:不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。
*数据依赖性
:如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。
3).处理器指令重排
处理器指令重排和编译器指令重排原理比较类似,都只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性。
4).内存系统重排序
现代的处理器使用写缓冲区
来临时保存向内存写入的数据。
写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。
同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。
虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。
这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!
先看一下下面的例子
线程A | 线程B |
---|---|
a = 1; (A1) | b = 2; (B1) |
x = b; (A2) | y = a; (B2) |
与上文编译器指令重排的例子相似,这里的指令执行是无序的
为方便研究,我们假定其顺序是A1,B1,A2,B2(初始状态:a = b = 0),则结果(x,y)应为(2,1)
但实际上就算顺序确定结果可能为(0,0),具体原因如下图
实际操作:
1. 处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1)
2. 从内存中读取另一个共享变量(A2,B2)
3. 把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)
4. 程序得到x = y = 0的结果。
从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。
虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。
此时,处理器A的内存操作顺序被重排序了(处理器B的情况和处理器A一样,这里就不赘述了)。
这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操做重排序。
内存屏障指令
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令
来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。
2.原子性
原子性指的是一个操作是不可中断
的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。
3.可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程能够马上得知
这个修改的值。
- 串行环境: 没有可见性问题,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
- 并行环境:由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。
4.有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序
行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序
的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
三.Java内存模型的保障机制
Java内存模型给予了程序员承诺,并通过synchronized,volatile 关键字以及Happens-Before机制(先行发生原则)来保障
1.Happens_Before(先行发生原则)
从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。
在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。
先行发生原则是Java内存模型的基石,是Java内存模型的基本守则,无须任何额外手段就能保障成立,而对于这些规则之外的,请不要依靠直觉认为他们是有序的(除非你自己通过其他手段保障),默认情况下,除了以下8条规则所保障的情况,其他的都应该认为是无序发生的.
需要注意的是,happens-before是对已有的顺序进行保证,即在已有顺序的前提下保证顺序的正确执行,本身并没有创造出新的顺序,对happens-before的正确理解应该是:在heppens-before的范围内,如果A先于B,那么对我来说A肯定会先于B
程序次序规则
(Program Order Rule)
在一个线程内必须保证语义串行性,也就是说按照代码控制流顺序执行。锁规则
(Monitor Lock Rule)
解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前
也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。volatile规则
(Volatile Variable Rule)
volatile变量的写,先发生于读
也就是说,如果对于一个volatile变量先写再读,那么读的动作一定在写的动作之后
这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。传递性
(Transitivity)
A先于B ,B先于C 那么A必然先于C线程启动规则 (Thread Start Rule)
线程的start()方法先于它的每一个动作
即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见线程终止规则 (Thread Termination Rule)
线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。线程中断规则 (Thread Interruption Rule)
对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。对象终结规则 (Finalizer Rule)
对象的构造函数执行,结束先于finalize()方法
注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,它只需展现出来是如此即可.
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。
对于java程序员来说,happens-before规则简单易懂,它避免程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。
如上图所示,一个happens-before规则通常对应于多个编译器重排序规则和处理器重排序规则。
2.volatile
volatile有两大特性,可见性
和禁用指令重排序
,其中禁用指令重排序通过插入内存屏障指令实现,属于JSR-133对volatile内存语义增强后的新特性,两者保证了volatile的happens-before机制
两者的目的是不同的:
可见性是对于不同线程间同一volatile变量而言的
禁用指令重排序则是对同一进程下volatile操作及前后(可能是普通变量,也可能是volatile变量)操作的顺序而言的
注意:
1. 在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量之间重排序,故旧的Java内存模型只支持可见性
2. 虽然有些文章写道volatile具有原子性(对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性),但这常常使人混淆概念.应该说volatile变量的读写操作具有原子性,而一般讨论原子性并不会单单指一个读或写操作,而是说一个方法或代码块(复合操作)的执行具有原子性,而这一点volatile没办法给我们提供保障
1).可见性
保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。
可见性由volatile的写读内存语义维护
可见性是对于不同线程间同一volatile变量而言的
volatile的写读内存语义
从内存语义的角度来说,volatile与监视器锁有相同的效果:
- volatile写和锁的释放有相同的内存语义;
- volatile
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
。 - 锁
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
- volatile
- volatile读与锁的获取有相同的内存语义。
- volatile
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
。 - 锁
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量
- volatile
volatile内存语义的实质:实现通信
过程 | 实质 |
---|---|
线程A写一个volatile变量 | 线程A发出消息 :线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。 |
线程B读一个volatile变量 | 线程B接收消息 :是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。 |
线程A写一个volatile变量,随后线程B读这个volatile变量 | 线程A对线程B通信 :线程A通过主内存向线程B发送消息。 |
2).禁止指令重排序优化
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得到正确的结果(as-if-serial),而不会保证变量赋值操作的顺序与程序代码中的执行顺序一致.而volatile变量则通过插入内存屏障来禁用这种指令重排序优化,使之与程序代码中的执行顺序一致.
1.禁用指令重排序规则
是否能重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
即:
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后
。 - 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前
。 - 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
2.volatile与单例模式
在JDK1.5之前,双重检查锁(DCL)是不安全的(即使用上volatile)
public class Singleton {
private volatile static Singleton myInstance = null;
private Singleton () {
}
public static Singleton getInstance() {
if(myInstance== null) {
synchronized(Singleton.class) {
if(myInstance== null) {
myInstance= new Singleton();
}
}
}
return myInstance;
}
}
单线程下没问题,但多线程时就可以出现线程安全问题。
原因:某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为myInstance= new Singleton();可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
myInstance= memory; //3.设置myInstance指向刚分配的内存地址,此时myInstance!=null
由于步骤1和步骤2间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间
myInstance= memory; //3.设置instance指向刚分配的内存地址,此时myInstance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
- JSR-133之前
虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量之间重排序。 - JSR-133之后:
增强了volatile的内存语义,根据其指令重排序禁用规则,当第二个操作是volatile写(myInstance= memory;)时,不管第一个操作是什么,都不能重排序.所以步骤3之前的步骤不能重排序后步骤3之后
3).volatile的happens-before关系
volatile变量的写,先发生于读
也就是说,如果对于一个volatile变量先写再读,那么读的动作一定在写的动作之后
这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
需要注意的是,多线程的情况下,一个线程写,一个线程读,两个线程同时start,写不一定先于读
这是因为线程的读操作本身可能先于线程的写操作
另外,volatile的happens-before也需要其第二个特性的支持,否则无法保证,后文会再作说明
请看下面使用volatile变量的示例代码:
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}
假设线程A执行writer()方法之后,线程B执行reader()方法。(也有可能线程B执行reader()方法之后线程A才执行writer()方法,这种情况下语句4不执行)根据happens before规则,这个过程建立的happens before 关系有:
- 根据程序次序规则,1 happens before 2; 3 happens before 4。
- 根据volatile规则,2 happens before 3。
- 根据传递性规则,1 happens before 4。
上述happens before 关系的图形化表现形式如下:
在上图中,每一个箭头链接的两个节点,代表了一个happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens before保证。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
增强volatile的内存语义对happens-before的保证
在旧的内存模型中,上面示例程序可能被重排序成下列时序来执行:
因此在旧的内存模型中 ,volatile的写-读没有监视器的释放-获所具有的内存语义。为了提供一种比监视器锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和监视器的释放-获取一样,具有相同的内存语义。
参考:
《深入理解Java虚拟机》
《Java并发编程实战》
《Java并发编程的艺术》