JMM-java内存模型图文详解
一、JMM定义
JMM定义了一组规则或规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的。实际上,JMM提供了合理的禁用缓存以及禁止重排序的方法,所以其核心的价值在于解决可见性和有序性。
Java内存模型规定所有的变量都存储在主存中,每个Java线程都有自己的工作内存。
Java内存模型定义的两个概念:
- 主存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主存中,无论该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括共享的类信息、常量、静态变量。由于是共享数据区域,因此多条线程对同一个变量进行访问可能会发现线程安全问题。
- 工作内存:主要存储当前方法的所有本地变量信息(工作内存中存储着主存中的变量副本),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,即使两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括字节码行号指示器、相关Native方法的信息。注意,由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
Java内存模型的规定如下:
(1)所有变量存储在主存中。
(2)每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。
(3)不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递。
在JMM中,Java线程、工作内存、主存之间的关系大致如图所示。

JMM将所有的变量都存放在公共主存中,当线程使用变量时,会把公共主存中的变量复制到自己的工作内存(或者叫作私有内存)中,线程对变量的读写操作是自己的工作内存中的变量副本。因此,JMM模型也需要解决代码重排序和缓存可见性问题。JMM提供了一套自己的方案去禁用缓存以及禁止重排序来解决这些可见性和有序性问题。JMM提供的方案包括大家都很熟悉的volatile、synchronized、final等。JMM定义了一些内存操作的抽象指令集,然后将这些抽象指令包含到Java的volatile、synchronized等关键字的语义中,并要求JVM在实现这些关键字时必须具备其包含的JMM抽象指令的能力。
二、与JVM的区别
JMM(Java内存模型)看上去和JVM(Java内存结构)差不多,很多人会误以为两者是一回事,这也就导致面试过程中经常答非所问。
JMM属于语言级别的内存模型,它确保了在不同的编译器和不同的CPU平台上为Java程序员提供一致的内存可见性保证和指令并发执行的有序性。
以Java为例,一个i++方法编译成字节码后,在JVM中是分成以下三个步骤运行的:
(1)从主存中复制i的值并复制到CPU的工作内存中。
(2)CPU取工作内存中的值,然后执行i++操作,完成后刷新到工作内存。
(3)将工作内存中的值更新到主存。
当多个线程同时访问该共享变量i时,每个线程都会将变量i复制到工作内存中进行修改,如果线程A读取变量i的值时,线程B正在修改i的值,问题就来了:线程B对变量i的修改对线程A而言就是不可见的。这就是多线程并发访问共享变量所造成的结果不一致问题,该问题属于JMM需要解决的问题。
JMM属于概念和规范维度的模型,是一个参考性质的模型。JVM模型定义了一个指令集、一个虚拟计算机架构和一个执行模型。具体的JVM实现需要遵循JVM的模型,它能够运行根据JVM模型指令集编写的代码,就像真机可以运行机器代码一样。
虽然JVM也是一个概念和规范维度的模型,但是大家常常将JVM理解为实体的、实现维度的虚拟机,通常是指HotSpot VM。
三、JMM与硬件内存架构是什么样的关系
对于硬件内存来说只有寄存器、缓存内存、主存的概念,并没有工作内存(线程私有数据区域)和主存(堆内存)之分。也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,无论是工作内存的数据还是主存的数据,对于计算机硬件来说都会存储在计算机主存中,当然也有可能存储到CPU高速缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
JMM与硬件内存架构的对应关系如图所示。

四、JMM的8个操作
Java内存模型规定所有的变量都存储在主存中(类似于前面讲的主存或者物理内存),每个线程都有自己的工作内存(类似于CPU中的高速缓存)。工作内存保存了线程使用到的变量的拷贝副本,线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。
JMM定义了一套自己的主存与工作内存之间的交互协议,即一个变量如何从主存拷贝到工作内存,又如何从工作内存写入主存,该协议包含8种操作,并且要求JVM具体实现必须保证其中每一种操作都是原子的、不可再分的。

如果要把一个变量从主存复制到工作内存,就要按顺序执行Read和Load操作;如果要把变量从工作内存同步回主存,就要按顺序执行Store和Write操作。
说明:JMM要求Read和Load、Store和Write必须按顺序执行,但不要求连续执行。也就是说,Read和Load之间、Store和Write之间可插入其他指令。
JMM主存与工作内存之间的交互协议的8个操作之间的关系如图4-14所示。

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。不允许read和load、store和write操作之一单独出现,意味着有read就有load,不能读取了变量值而不予加载到工作内存中;有store就有write,也不能存储了变量值而不写到主存中。
(2)不允许一个线程丢弃它最近的assign操作,也就是说当线程使用assign操作对私有内存的变量副本进行变更时,它必须使用write操作将其同步到主存中。
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主存中。
(4)一个新的变量只能从主存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use和store操作之前,必须先执行assign和load操作。
(5)一个变量在同一个时刻只允许一个线程对其执行lock操作,但lock操作可以被同一个个线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(7)如果一个变量实现没有被lock操作锁定,就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量。
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主存(执行store和write操作)。
以上JMM的8大操作规范定义相当严谨,也极为烦琐,JVM实现起来也非常复杂。Java设计团队大概也意识到了这个问题,新的JMM版本不断地对这些操作进行简化,比如将8个操作简化为Read、Write、Lock和Unlock四个操作。虽然进行了简化,但是JMM的基础设计并未改变。
五、JMM如何解决有序性问题
JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序(不是所有的编译器重排序都要禁止)。
1、JMM内存屏障
由于不同CPU硬件实现内存屏障的方式不同,JMM屏蔽了这种底层CPU硬件平台的差异,定义了不对应任何CPU的JMM逻辑层内存屏障,由JVM在不同的硬件平台生成对应的内存屏障机器码。JMM内存屏障主要有Load和Store两类,具体如下:
- Load Barrier(读屏障)在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存加载数据。
- Store Barrier(写屏障)在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。
在实际使用时,会对以上JMM的Load Barrier和Store Barrier两类屏障进行组合,组合成LoadLoad(LL)、StoreStore(SS)、LoadStore(LS)、StoreLoad(SL)四个屏障,用于禁止特定类型的CPU重排序。
(1)LoadLoad(LL)屏障
在执行预加载(或支持乱序处理)的指令序列中,通常需要显式地声明LoadLoad屏障,因为这些Load指令可能会依赖其他CPU执行的Load指令的结果。一段使用LoadLoad(LL)屏障的伪代码示例如下:

该示例的含义为:在Load2要读取的数据被访问前,使用LoadLoad屏障保证Load1要读取的数据被读取完毕。
(2)StoreStore(SS)屏障
通常情况下,如果CPU不能保证从高速缓冲向主存(或其他CPU)按顺序刷新数据,那么它需要使用StoreStore屏障。一段使用StoreStore(SS)屏障的伪代码示例如下:

该示例的含义为:在Store2及后续写入操作执行前,使StoreStore屏障保证Store1的写入结果对其他CPU可见。
- LoadStore(LS)屏障该屏障用于在数据写入操作执行前确保完成数据的读取。一段使用LoadStore(LS)屏障的伪代码示例如下:

该示例的含义为:在Store2及后续写入操作执行前,使LoadStore屏障保证Load1要读取的数据被读取完毕。
- StoreLoad(SL)屏障该屏障用于在数据读取操作执行前,确保完成数据的写入。使用LoadStore(LS)屏障的伪代码示例如下:

该示例的含义为:在Load2及后续所有读取操作执行前,使StoreLoad屏障保证Store1的写入对所有CPU可见。
StoreLoad(SL)屏障的开销是4种屏障中最大的,但是此屏障是一个“全能型”的屏障,兼具其他3个屏障的效果,现代的多核CPU大多支持该屏障。
五、volatile语义中的内存屏障
参考volatile文章
六、补充了解
变量共享
JVM的运行时内存布局,总体上可以分为堆内存与线程栈内存。堆内存存储全局对象数据以实现多线程共享,栈内存存储线程执行的相关信息,栈内存无法共享
所有对象实例、static字段、数组元素都存储在堆内存中,能够实现线程共享。基本数据类型的本地变量都完全存储在线程堆栈中,无法实现线程共享。方法的输入参数以及异常信息无法在多线程之间共享。一个线程可以将基本数据类型的变量作为入口参数传递给另一个线程,但是它不能共享局部变量本身。异常信息属于线程私有信息,无法跨线程传递。
变量共享的内存可见性
JMM对共享变量的可见性做了如下描述。当多个线程共享同一个变量,一个线程对普通变量做出修改,其他线程也可以感知到,但不是实时感知的,同步时间依赖于程序的调度与CPU缓存自身的同步机制。被volatile关键字修饰的变量在修改时能够实现线程间的实时可见。对象加锁和解锁操作是实时可见的,一个线程对一个对象加锁,另外一个线程能够实时感知。一个线程的启动与停止对另一个线程是实时可见的,一个线程能实时感知到另一个线程启动与结束的状态。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)