Java内存模型
在了解Java内存模型之前应该先了解一下物理计算机的并发模型,因为计算机的并发模型,和JAVA虚拟机的内存模型是由很高的可比性的。
计算机的并发模型:
1–>首先需要明白的一点是:大多数的运算任务都不可能仅仅靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等。
2–>因为计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不假如一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓存,具体用法:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后在从缓存中同步到内存中。
3–>但是这样的结构同时也会产生一个问题:缓存一致性(Cache Coherence)在多处理器系统中,每个处理器都有自己的高速缓存,而它们现在又共用一个主内存(Main Memory),当多个处理器的计算任务涉及到同一块主存区域时,将可能导致各自的缓存数据不一致。
4–>为了解决缓存一致性问题,需要各个处理器访问缓存时都遵守一些协议,在都写时要根据协议来进行操作。
所以计算机的处理器、高速缓存、主内存之间的交互关系是这个样子的:
现在正式开始说Java内存模型
Java内存模型
1–>首先Java内存模型的出现,是Java虚拟机为了屏蔽掉各种硬件和操作系统的内存访问的差异,以实现Java的无平台限制的效果。
2–>在JDK1.5发布之后,Java内存模型才开始慢慢的完善起来。
3–> Java内存模型主要的目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。(这里的变量和我们Java程序中的变量不太一样,这里的变量指的是实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的)
主内存和工作内存:
1–> Java内存模型规定了所有的变量都存储在主存中(Main Memory)。每条线程都有自己的工作内存(Working Memory,类似与上面计算机模型中的高速缓存)。
2–>线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。
3–>线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
4–>不同的线程之间无法直接访问对方的工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
线程、主内存、工作内存三者之间的交互关系是这个样子的:
JMM的线程、主内存、工作内存的交互关系和上面的计算机并发模型中的处理器、高速缓存、主内存之间的交互关系真的是惊人的相似。(怀疑,JVM的设计者是不是模仿了计算机的并发模型)
注:这里的主内存和工作内存与Java内存空间中的堆、栈、方法区等并不是同一个层次的内存划分。
内存间的交互操作:
关于主内存和工作内存之间具体的交互协议,Java内存模型定义了以下8中操作来完成。虚拟机在实现时,必须保证这8种操作都是原子性的、不可再分的。
English | 中文 | 作用区间 | 具体操作 |
---|---|---|---|
lock | 锁定 | 主内存 | 它把一个变量标识为一条线程独占的状态 |
unlock | 解锁 | 主内存 | 它把一个处于锁定状态的变量释放出来,解锁以后才能再次锁定 |
read | 读取 | 主内存 | 它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load使用 |
load | 载入 | 工作内存 | 他把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
use | 使用 | 工作内存 | 把工作内存中的一个变量的值传递给执行引擎 |
assign | 赋值 | 工作内存 | 把一个从执行引擎中接收的值赋给工作内存的变量 |
store | 存储 | 工作内存 | 把工作内存中的一个变量的值传递到主内存中,以便write的使用 |
write | 写入 | 主内存 | 把store操作从工作内存中得到的变量的值放入主内存的变量中 |
在不触犯下面的规则的基础上,指令重排同样会作用与上面的指令。
除此之外Java内存模型还规定了在执行上述8种操作时必须满足如下规则:
8条规则 |
---|
1.不允许read和load、store和write操作之一单独出现(不允许一个变量从主内存种读了,但是工作内存不接受或者从工作内存写了,主内存不接受) |
2.不允许一个线程丢弃它最近的assign操作(变量在工作内存种改了之后必须将其同步回主内存中) |
3.不允许一个线程无原因的(没有进行过assign操作)把数据从工作内存写回到主内存。 |
4.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量。(对一个变量实施use、store操作之前,必须执行过load、assign操作) |
5.一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程执行多次。(这个和对象锁还有类锁的性质是差不多的) |
6.如果对一个变量进行lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化变量的值。 |
7.如果一个变量事先没有被lock操作锁定,那就不允许对他进行unlock操作,也不允许unlock一个被其它线程锁定住的变量 |
8.对一个变量执行unlock之前,必须把此变量同步回主内存中(执行store、write操作) |
上述的8种内存访问操作及8条约束条件,再加上一些特殊的规定,就已经完全可以确定了Java程序种哪些内存访问操作在线程下是安全的。
上面说到的特殊的规定,就是对volatile关键字特殊的规定:
对volatile型变量的特殊规定:
1·当一个变量被定义为volatile类型之后,它将具备两种特性:a.保证此变量对所有线程的可见性,b.禁止指令重排序优化
2·volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立即反应到其它线程之中,换句话说,volatile变量在各个线程中是一致的。
3·volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但是由于每次使用之前都要刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题)
4·由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们依然要通过加锁来保证原子性:
a·运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
b·变量不需要与其它的状态变量共同参与不变约束
拓展:volatile底层的实现:
首先,将操作用volatile修饰的变量的代码翻译成汇编指令之后,会发现,在正常操作的基础上,使用volatile修饰的变量多加了一条lock操作,那么这个操作是什么意思呢?这个操作相当于一个内存屏障(Memory Barrier 指令重排序不能把内存屏障后面的指令重排序到内存屏障之前的位置)。lock操作实际上是做了什么呢?它使得本CPU中的高速缓存(Cache)写入了内存(Main Memory),该写入动作也会引起别的CPU或者别的内核无效化其缓存。当无效之后,下一次访问该变量的时候,就必须再从主存中重新获取,以这样的方式强制使得变量同步。
5·至于禁止指令重排序,是因为指令重拍无法越过内存屏障,正因为如此,volatile关键字,需要再本地代码中插入许多的内存屏障指令来保证处理器不发生乱序执行。
Java内存模型的特性:
·原子性
Java内存模型直接保证原子性变量操作的包括:read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果需要更大范围的原子保证,Java内存模型还提供了lock和unlock操作来满足需求,虽然虚拟机没有将这两个操作直接开放给用户使用,但是这两个操作在Java代码中的反映就是 synchronized 关键字,所以 synchronized 块之间的操作也具备原子性。
·可见性
可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,无论是volatile变量还是普通变量都是如此。它们之间的区别在于:volatile的特殊性可以保证新值能立即同步到主内存。所以可以这么说,volatile保证了多线程情况下的可见性,但是普通变量就不能保证这一点了。除了volatile,synchronzied和final也能保证可见性。
·有序性
程序中天然具有一定的有序性,天然的有序剋总结为一句话:“如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另外一个线程,所有的操作都是无序的。”前半句是指线程内表现为串行的语义,后半句是指指令重排序现象和工作内存与主内存同步延迟的现象。
Java中提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile关键字本身就包含了禁止指令重排序的语义。而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的。
先行发生原则(happens-before)
我们上面已经说了那么一大堆的有序性,那我们可能就回想到有一个问题,我们在日常写代码的时候也没有使用volatile和 synchronized两个关键字来使得我们的代码有序呀,但是我们的代码为什么就没有乱掉呢?
这是因为在Java语言中有一个“先行发生(happens-before)”的原则。这个原则非常的重要(这就是没有让你的代码乱掉的东西呀,那是相当的重要呀),它是判断数据是否存在竞争、线程是否安全的主要依据。“先行发生”原则指的是什么?
先行发生原则是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是在说发生操作B之前,操作A产生的影响能被操作B观察到。就是说操作A与操作B有数据依赖关系。
下面是Java内存模型下的一些“天然的”先行发生关系:
名称 | 英文 | 解释 |
---|---|---|
程序次序规则 | Program Order Rule | 在一个线程内按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说应该是控制流(顺序、分支、循环)而不是代码顺序。 |
管理锁定规则 | Monitoe Lock Rule | 一个unlock操作先行发生于后面对同一个锁的lock操作。是同一个锁。 |
volatile变量规则 | Volatile Variable Rule | 对一个volatile变量的写操作先行发生于对这个变量的读操作。 |
线程启动规则 | Thread Start Rule | Thread类的start()方法先行发生于此线程的每一个动作 |
线程终止规则 | Thread Termination Rule | 线程的所有操作都先行发生于对此线程的终止检测。 |
线程中断检测 | Thread Interruption Rule | 对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生。 |
对象终结规则 | Finalizer Rule | 一个对象的初始化完成(构造函数执行结束)先行发生于它的fnialize()方法的开始。 |
传递性 | Transitivity | 如果A操作先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。 |
Java中无需任何的同步操作保障能成立的先行发生队则就只有上面的8种了。