一、引言
本篇文章将从计算机硬件、操作系统、Java语言,一环扣一环地引出Java内存模型存在的意义,让大家对Java内存模型(JMM)有较为深刻的理解。
二、CPU&&内存
众所周知CPU和内存是计算机中两个重要的组成部分,其中CPU负责计算,内存负责存储数据,每次CPU计算前都需要从内存获取数据。我们知道CPU的运行速度远远快于内存的速度,因此会出现CPU等待内存读取数据的情况。
由于两者的速度差距实在太大,为了加快运行速度,于是计算机的设计者在CPU中加了一个CPU高速缓存。这个CPU高速缓存的速度介于CPU与内存之间,每次需要读取数据的时候,先从内存读取到CPU缓存中,CPU再从CPU缓存中读取。
随着多核技术的出现,CPU的计算能力进一步提高。原本同一时间只能运行一个任务,但现在可以同时运行多个任务。多核CPU的出现,提高了CPU的处理速度,同时也带来了新的问题:缓存一致性。
多核系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个CPU的运算任务都涉及同一块主内存区域时,可能导致各自的缓存数据不一致。如果发生了这种情况,那同步回主内存时以哪个CPU高速缓存的数据为准呢?
首先明确这个是硬件层面的问题,与具体的操作系统和编程语言无关,那么这就需要CPU厂商来做这个事,答案是“缓存一致性协议”。
所谓的缓存一致性协议,指的是在CPU高速缓存与主内存交互的时候,遵守特定的规则,这样就可以避免数据一致性问题了。
在不同的CPU中,会使用不同的缓存一致性协议。例如,MESI协议用于奔腾系列的CPU中,而MOSEI协议则用于AMD系列CPU中,Intel的core i7处理器使用MESIF协议。
至于协议的具体内容,大家可以自行用搜索引擎检索。无外乎是定义了一些内存状态,然后通过消息的方式通知其他CPU高速缓存,从而解决了数据一致性的问题
三、操作系统
操作系统,它屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化,方便我们进行上层软件的开发,它将硬件进行封装,然后抽象出一些概念,例如CPU时间片、内核态、用户态等。
因此,操作系统抽象出来的CPU与内存也会面临这样的问题——缓存数据一致性。通过把在特定的操作协议下,对特定内存或高速缓存进行读写访问的过程进行抽象,得到的就是操作系统内存模型。无论是macOS,还是linux,它们都有着自己特定的内存模型。
Java语言是建立在操作系统上层的高级语言,它只能与操作系统进行交互,而不与硬件进行交互。与操作系统相对于硬件类似,操作系统需要抽象出内存模型,那么Java语言也需要抽象出相对于操作系统的内存模型。一般来说,编程语言也可以直接复用操作系统层面的内存模型,例如,C++语言就是这么做的。但由于不同操作系统的内存模型不同,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错。因此在某些场景下,就必须针对不同的平台来编写程序。
四、JMM
JMM即Java Memory Model,那么为什么会有它呢?大多数人刚学Java时常会听到的一句英文——“Write Once, Run Anywhere”,简单的说就是为了屏蔽各种硬件或者系统内存的访问差异,实现了Java程序在各种平台都能达到一致的内存访问效果。完美解决上一小节所说的针对不同的平台来编写程序问题。
如果简单的描述JMM定义,那么它是这样的,下图描述了线程、工作内存与主内存的关系,
这里明确一点,通常说的JVM内存模型定义的堆、栈、程序计数器等的划分与JMM不是同一层次的内存划分。Java内存模型定义了Java语言如何与内存进行交互,具体地说是Java语言运行时的变量,如何与我们的硬件内存进行交互的。而JVM内存模型,指的是JVM内存是如何划分的。
这里我们也可以立个Flag,未来的某一章研究一下JVM内存模型。
JMM的主要目标是定义程序中变量的访问规则,所有的共享变量都存储在主内存中,每个线程都有自己的工作内存,工作内存中保存的是主内存中变量的副本,线程对变量的读写等操作必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
这里我们再简单的看下Java定义内存间的交互操作有哪些?要考,仔细读,
JMM总共定义了8种操作,
操作 | 作用域 | 用途 |
---|---|---|
lock | 主内存 | 把一个变量标识为一条线程独占的状态 |
unlock | 主内存 | 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 |
read | 主内存 | 把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用 |
load | 工作内存 | 把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
use | 工作内存 | 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作 |
assign | 工作内存 | 把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
store | 工作内存 | 把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用 |
write | 主内存 | 把store操作从工作内存中得到的变量的值放入主内存的变量中 |
不难看出这些操作有些是成对出现的,例如read和load、store和write操作是不允许单独出现的,即不能发生一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况。
除了上述的规则外,还有其他的规则如下,
- 一个线程不能丢弃它的最近的assign操作
- 没有发生过任何assign操作,数据不能从工作内存同步回主内存中
- 一个变量实施use、store操作之前,必须先执行过了assign和load操作
- 同一个时刻只允许一条线程对其进行lock操作,同一线程可重复lock
- 对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
- unlock操作需要在lock之后,一个被其他线程lock的变量只能有其他线程unlock
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)
五、总结
- 多核CPU和高速缓存在,导致了缓存一致性问题,通过缓存一致性协议解决。
- 各个操作系统都有各自内存模型,对CPU高速缓存与缓存的读写访问过程进行抽象。
- Java语言作为运行在操作系统层面的高级语言,为了解决多平台运行的问题,在操作系统基础上进一步抽象,得到了 Java 语言层面上的内存模型。