Java内存模型

Java内存模型目标在于定义程序中变量的访问规则,Java内存模型分为主内存与工作内存。

谈论Java内存模型绕不开线程安全问题,所以有必要明确一下线程安全的定义:当多个线程访问一个对象时,如果不用考虑调度和交替执行,也不需要进行额外的同步,而这个对象的行为结果正确。

所有的变量都在主内存,而线程操作变量时,需要把变量从主内存读取到工作内存,然后送到执行引擎进行计算,再把计算结果放回工作内存,最后写回主内存。

这里所说的主内存、工作内存与经常提到的堆、栈、方法区并不是同一个层面上的概念。因为java内存模型(JMM)是为了解决原子性、有序性、可见性这些问题应运而生的规则,可能因为这两种划分中,都有共享数据区域和私有数据区域的概念,所以会联想到它们之间的是否存在一一对应的关系。

JMM中关于内存划分

主内存:

存储Java实例对象,不管该实例对象是成员变量、方法中的局部变量、常量、静态变量。

工作内存:

存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的。

因为JMM只是一种抽象的概念,是一组规则,不管是工作内存,还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中、CPU缓存或者寄存器中,因此,Java内存模型,和计算机硬件内存架构,是一个相互交叉的关系,是一种抽象概念与真实物理硬件的交叉。(对于Java内存区域划分也是同样的道理)

Java内存模型间交互

对Java内存模型作完简单描述后,看看Java内存模型间交互,从主内存读到数据到工作内存,然后送到引擎进行计算,最后,把计算结果按来时原路返回,看看JMM如何定义这些步骤。

lock(锁定):

用于主内存的变量,把一个变量标识为线程独占状态。

unlock(解锁):

用于主内存变量,把处于锁定状态的变量释放出来。

read(读取):

用于主内存变量,把一个变量值从主内存传输到线程的工作内存中。

load(载入):

用于工作内存的变量,把read操作得到的变量值,放入工作内存的变量中。

use(使用):

用于工作内存的变量,把工作内存中变量值,传递给执行引擎。

assign(赋值):

用于工作内存的变量,把执行引擎的值,赋给工作内存的变量。

store(存储):

用于工作内存的变量,把工作内存中的变量的值,传送到主内存中。

write(写入):

用于主内存的变量,它把store操作的值,传送到主内存的变量中。

这个过程中的步骤不一定连续,但顺序必需保证,所以就有了以下规则:

1、不允许read和load、store和write操作其中之一单独出现。

2、不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了后,必须同步到主内存中。

3、不允许一个线程无原因地(没有发生过任何assign操作),把数据从工作内存同步回主内存中。

4、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量。即:对一个变量实施use和store操作之前,必须先执行了assign和load操作。

5、一个变量在同一时刻只允许一个线程对其进行lock操作,lock和unlock必须成对出现,如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

6、如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。对一个变量执行unlock操作之前,必须先把此变量同步到主内存中。

这个漫长的交互过程,总会出现交替执行的可能,所以需要有一种机制,去满足一些不允许交替执行的场景。这就是Java中的synchronized和volatile关键字出现的原因。

volatile并不具备锁定功能,它只是加强了数据的可见性,并不能保证一致性。synchronized关键字能同时满足三个条件,而volatile只能满足其一,很多时候被误认为是轻量级的锁。

java指令重排序

由于编译优化与处理器指令优化,很多指令在最终执行时的顺序,可能与实际编码的顺序不一致,这种不一致在单线程模式下并不会引发逻辑错误。但在多线程模式下,就有可能是问题的根源,所以,为了应对这种情况,java内存模型给出了一些解决方案,如同步代码块synchronized,final关键字的保证、volatile的保证。

除上以上需要明确声明的保障机制,java内存模型默认有一套规则,用于辅助保证原子性、可见性以及有序性。它就是happens-before 原则 。

关于java内存模型详细说明参考官方文档