内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
为了解决处理器与内存速度之间的矛盾,引入了高速缓存的存储交互方式,但同时也引入了新的问题:缓存一致性,当多个处理器的运算任务搜涉及到一块主内存区域时,可能导致各自的存储数据不一致,为了解决缓存一致性问题,需要在各个处理器访问缓存时遵循一些协议,这类协议有MSI,MOSI,MESI等
Java内存模型
主流程序语言(C、C++)直接使用物理硬件和操作系统的内存模型,由于不同平台上的内存模型的差异,有可能导致一套程序在另一平台上的访问出错。
主内存与工作区
Java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节,这里的变量指的是实例字段,静态字段等共享的数据,但不包括局部变量与方法参数,这是线程私有的,自然不会有竞争问题。
- Java内存模型规定所有变量储存在主内存(类比物理硬件主内存,但此处仅仅是虚拟机内存的一部分,主要对应Java堆实例对象数据部分)
- 每条线程有自己的工作内存(类比高速缓存,对应虚拟机栈的部分区域)
- 工作内存保存线程使用的变量的主内存的副本拷贝,线程对变量的所有操作在工作内存进行,不能直接读取主内存变量
- 不同线程之间无法直接访问对方的工作内存
- 线程之间的变量值传递均需要通过主内存完成。
内存之间的交互
lock(锁定)
作用于主内存的变量,将变量标识为一条线程独占的状态
unlock(解锁)
作用于主内存的变量,将一个处于锁定状态的变量释放出来,释放后的变量才能被其它线程锁定
read(读取)
作用于主内存的变量,将主内存的中的变量值传输到线程的工作区域中,以便后续的load操作
load(载入)
作用于主内存的变量,将由read得到的变量值放入到工作内存的变量副本中
use(使用)
作用于主内存的变量,将工作内存中的一个变量值传递到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行操作
assign(赋值)
作用于主内存的变量,将从执行引擎接受到的值赋给工作内存的变量。
store(储存)
作用于主内存的变量,将工作内存的一个变量值传送到主内存中,以便随后的write操作
write(写入)
作用于主内存的变量,将store操作从工作区得到的变量值放入到主内存的变量中
在执行以上8种操作时需要满足的规则:
-
不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
-
不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
-
不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
-
一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
-
一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
-
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
-
如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
-
对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
对于volatile型变量的特殊规则
关键字volatile是Java虚拟机提供的最轻量级的同步机制。
当一个变量在被定义为volatile之后,具备两种特性,第一是保证此变量对所有线程的可见性,这里的‘可见性’指当一条线程修改了这个变量的值,新值对所有线程是立即可以得到的,普通变量需要通过主内存完成。
volatile只能保证可见性,其变量在并发情况下一样是不安全的,在不符合以下两条规则的运算情景中,我们任然需要通过加锁来保证原子性。
- 运算结果并不依赖于变量的当前值,或者能确保只有单一的线程修改变量的值
- 变量不需要与其他变量的状态变量共同参与不变约束
使用volatile变量的第二个语义是禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作顺序与程序代码的执行顺序一致。
volatile变量的读操作性与一般的变量没有什么区别,但写操作性可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令(指重排序时不能将后面的指令重排序到内存屏障之前的位置)来保证处理器不会放生乱序执行。
对于long与double型变量的特殊规则
Java内存模型要求8个操作都具有原子性,但是对于64位数据类型:允许虚拟机将没有volatile修饰的64位数据类型的读写操作划分为两次32位操作进行,即虚拟机实现选择可以不保证64位数据类型的load、store、read、write这4个操作的原子性。
目前各种平台商用虚拟机都将64位数据的读写操作作为原子性操作来对待,因此编写代码时一般不需要把用到long、double的变量专门声明为volatile。
原子性、可见性、有序性
原子性
Java内存模型提供8个操作直接保证基本数据类型的访问读写具有原子性(long、double的非原子性知道就行了)
如果场景需=需要更大范围的原子性保证,Java内存模型提供unlock与lock满足需求,还有synchronized关键字之间的操作也具有原子性。
可见性
指当一个线程修改了共享变量的值,其他线程能立即得到这个值。Java内存模型是通过变量在修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。无论是普通变量还是volatile都是如此,只不过volatile的特殊规则保证新值能立即同步到主内存以及每次使用前立即从主内存刷新。synchronized同步块的可见性是由‘对一个变量执行unlock操作前,必须将此变量同步回主内存中’这条规则获得。final的可见性指:被修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,在其他线程中能看见final的值。
有序性
Java程序的有序性总结为:如果在本线程内观察,所有操作都是有序的(线程内表现为串行语义),如果在一个线程观察另一个线程,所有操作都是无序的(指令重排序、工作内存与主内存同步延迟)
Java提供synchronized与volatile保证线程操作的有序性,volatile本身就包含禁止指令重排序,synchronized由‘一个变量在同一个时刻只允许一条线程对其进行lock操作’这条规则获得