在了解Java内存模型之前,先来看一下多核硬件架构。
我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦。
刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。
所以,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。
再随着市场对CPU计算能力的需要,于是出现了多核CPU,每个核都有各自的缓存。下图简单描述了多核硬件架构的实现。
现代计算机硬件,几乎都是多核处理器实现。打开Windows的任务管理器,可以核心数,还可以看到处理器的缓存。如下图红框:
可以清晰的看到L1、L2、L3三级缓存。
计算机内部的缓存架构(CPU三级缓存)
缓存大大缩小了高速CPU与低速内存之间的差距。以三层缓存架构为例:
- L1 Cache最接近CPU, 容量最小(如32K、64K、256K等)、速度最高,每个核上都有一个L1 Cache。
- L2 Cache容量更大(如256K)、速度更低, 一般情况下,每个核上都有一个独立的L2 Cache。
- L3 Cache最接近内存,容量最大(如12MB),速度最低,在同一个CPU插槽之间的核共享一个L3 Cache。
单核时代只有一个处理器核心,读/写操作完全都是由单核完成,没什么问题;但是多核架构,一个核修改主存后,其他核心并不知道数据已经失效,继续傻傻的使用,轻则数据计算错误,重则导致死循环、程序崩溃等。
缓存一致性协议
多核CPU硬件架构厂商,设计之初就预测到多线程操作数据不一致的问题,因此出现了——缓存一致性协议。
不同的CPU硬件生产厂商,具体的实现不一样。Intel的MESI协议最出名。
MESI协议文档:https://en.wikipedia.org/wiki/MESI_protocol
在MESI协议中,每个Cache line有4个状态,可用2个bit表示,它们分别是:
状态 | 描述 |
---|---|
M(Modified) | 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 |
E(Exclusive) | 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。 |
S(Shared) | 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。 |
I(Invalid) | 这行数据无效。 |
在MESI协议中,每个Cache的Cache控制器不仅知道自己的读写操作,而且也监听(snoop)其它Cache的读写操作。每个Cache line所处的状态根据本核和其它核的读写操作在4个状态间进行迁移,如下图:
AMD的Opteron处理器使用从MESI中演化出的MOESI协议,O(Owned)是MESI中S和M的一个合体,表示本Cache line被修改,和内存中的数据不一致,不过其它的核可以有这份数据的拷贝,状态为S。
Intel的core i7处理器使用从MESI中演化出的MESIF协议,F(Forward)从Share中演化而来,一个Cache line如果是Forward状态,它可以把数据直接传给其它内核的Cache,而Share则不能。
用下面的简图,简单说明下MESI如何在多核环境,确保缓存一致性的。
两个CPU执行两个线程,都执行count++,都先从主内存获取count=0,都将count=0从主存拷贝到各自的缓存区,且在缓存区count=0的状态为S(Shared,多线程共享状态)。
一个线程(左边的线程)先执行了修改count+=1,按MESI缓存协议规范,在该线程中count是M状态(即:已被修改),则其他拥有count变量的线程,count状态都变为(I)失效状态,即需要再去主内存拿新值。
可见,缓存一致性协议解决了多核硬件架构的一致性问题。那缓存一致性又和JMM有什么关系呢?
JMM也是遵照多核硬件架构的设计,用Java实现了一套JVM层面的“缓存一致性”。
如果开发者运用的得当,最终可以达到Java线程间:变量可见性、有序性、原子性。
有了MESI,为什么还需要JMM?
既然有了MESI协议,是不是就不需要volatile的可见性语义了?当然不是,还有以下问题:
- 并不是所有的硬件架构都提供了相同的一致性保证,不同的硬件厂商实现不同,JVM需要volatile统一语义。
- 可见性问题不仅仅局限于CPU缓存内,JVM自己维护的内存模型(JMM)中也有可见性问题。使用volatile做标记,可以解决JVM层面的可见性问题。
Java内存模型JMM
Java内存模型(Java Memory Model,简称JMM),本身是种抽象的概念,并不是像硬件架构一样真实存在的;它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素
)的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建时,JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有数据,而Java内存模型中规定所有变量都存储在主内存,主内存时关系内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完后再将变量写回主内存,不能直接操作主内存中的变量。
工作内存是每个线程的私有数据区域,因此不同线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。如下图所示:
内存模型JMM与Java运行时数据区的关系
Java虚拟机的运行时数据区被划分为:方法区、Java堆、Java虚拟机栈、PC寄存器、本地方法栈,还有常量池。它们被分为两大类——线程共享和线程私有数据区。
线程共享数据区,包括:Java堆、方法区、常量池。它们会随着虚拟机启动而创建,随着虚拟机退出而销毁。
线程私有数据区,包括:PC寄存器、JVM栈、本地方法区。它们是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。详细了解可阅读:JVM体系结构-----深入理解内存结构
本质上来说,内存模型JMM和Java运行时数据区没有任何关系,它们是两个不同纬度的东西。
但如果硬要扯上关系的话,可以把Java运行时数据区的JVM栈 看作是内存模型中的"工作内存",因为按Java运行时数据区对内存的划分,符合线程私有性质、且存储本地变量的存储区域,只有线程私有的栈空间。
JMM主内存与工作内存的数据存储类型以及操作方式
根据虚拟机规范,对于一个实例对象中的成员方法而言
1、如果方法中包含本地变量是基本数据类型,将直接存储在工作内存(栈)中;
2、但如果本地变量是引用类型,那么该变量的引用会存储在工作内存的栈中,而对象的实例将存储在主内存(堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。
3、至于static变量以及类自身相关信息将会存储在主内存中。
需要注意的是,在主内存中的实例对象可以被多线程共享,如果两个线程同时调用了同一个对象的同一个方法,那么两个线程会将 要操作的数据拷贝一份到自己的工作内存中,执行完成后才刷新到主内存,如下图所示:
Java内存模型 与 硬件内存架构的关系
对于硬件架构来说,只有寄存器、缓存、主内存的概念,并没有工作内存(线程私有数据区)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上,Java内存模型和硬件内存架构师一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)
小结:
1、硬件架构的缓存一致性协议(如MESI),解决了多核硬件的一致性问题。
JMM也是遵照多核硬件架构的设计,用Java实现了一套JVM层面的“缓存一致性”。
如果开发者运用的得当,最终可以达到Java线程间:内存可见性、有序性、原子性。
2、JMM不像硬件架构,不是实实在在存在的,JMM是理论基础。但JMM最终还是要落到硬件架构上去运行。
存档文章
网友评论