深入理解Java虚拟机(十): 内存模型
内存模型
概念
内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程的抽象。
Java内存模型
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
主内存和与工作内存
主内存
Java内存模型规定了所有的变量都保存在主内存中。
工作内存
每个线程有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。
主内存和工作内存的关系
线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
不同的线程之间无法直接访问对方工作内存中的变量。
线程间变量值的传递均需要通过主内存来完成。
内存间交互操作
Java定义了以下8中操作来完成变量与主内存中间的操作,如变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存等。
Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(double和long类型的变量允许有例外)。
lock:锁定,作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock:解锁,作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read:读取,作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load:载入,作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use:使用,作用于工作内存中的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign:赋值,作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,没到虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store:存储,作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write:写入,作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
注:如果想要把变量从主内存中复制到工作内存中,那么就要按顺序执行read和load操作;如果是要把工作内存中的变量同步到主内存中,那么就要顺序执行store和write操作。但是Java虚拟机只规定这两个操作是必须按顺序执行,其他的可以不用。而且,这两组操作,不需要连续执行,比如可以这样:
Java虚拟机还规定了以下8种基本操作时的规则:
不允许read和load、store和write单独单一出现。即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起会写但是猪内存不接受的情况出现。
不允许一个线程丢弃它最近的assign的操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
不允许一个线程无原因地把数据从线程的工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量。
一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。
如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个呗其他线程锁定住的变量。
对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
volatile
volatile可以说是Java虚拟机提供的最轻量级的同步机制。当一个变量呗声名为volatile类型后,它将具备两个特性:
保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是立即得知的。但是,Java的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。 即valatile只能保证可见性,所以通常我们还是需要使用synchronized或java.util.concurrent中的原子来来保证原子性。 除了以下两种情况:
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其他的状态变量共同参与不变约束。
例:
分析结果:
上面的例子可以保证当shutdown被调用时,能保证所有线程中执行doWork()方法立即停止。
禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。而volatile关键字能够避免此类情况的发生。
例,如下伪代码:
代码分析:
如果initialized没有被volatile修饰,就可能由于指令重排序的优化,导致位于线程A中的最后一句代码initialized=true提前执行。
总结:
volatile变量读操作的性能消耗与普通变量并太大差别,但是写操作可能会慢一些,因为它需要再本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
对于long和double型变量的特殊规则
允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,也就是说多线程在调用没有被volatile修饰的long或者double型变量的时候可能调到半个变量的数值。但是这种情况非常罕见,因为Java虚拟机可以选择把这两个操作实现为具有原子性操作,而且基本上虚拟机也就这么做了。
原子性、可见性和有序性
原子性:由Java内存模型来直接保证的原子性操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。
可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取钱从主内存刷新变量值这种依赖主内存作传递媒介的方式来实现可见性的。 普通变量和volatile修饰的变量是一样的,只是volatile保证了新值能够立即同步到主内存,每次使用前立即从主内存刷新。
synchronized关键字保证可见性:对一个变量执行unlock之前,必须先把此变量同步回主内存中。
final关键字保证可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那在其他线程中就能看见final字段的值。
有序性:总结一下,如果在本线程中,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
先行发生原则
程序次序规则:在一个线程内,按照程序代码顺序(控制流顺序及逻辑顺序),前面的代码比后面的代码先行发生。
管程锁定规则:一个unlock操作先行发生于后面对于同一个锁的lock操作。
volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生,可以通过Thread.interrupt()方法检测到是否有中断发生。
对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就说明操作A先行发生于操作C。
本文转载自微信公众号【java学习之道】。
- 点赞
- 收藏
- 关注作者
评论(0)