Java 内存模型,许多人会错误地理解成 JVM 的内存模型。但实际上,这两者是完全不同的东西。Java 内存模型定义了 Java 语言如何与内存进行交互,具体地说是 Java 语言运行时的变量,如何与我们的硬件内存进行交互的。而 JVM 内存模型,指的是 JVM 内存是如何划分的。
Java 内存模型是并发编程的基础,只有对 Java 内存模型理解较为透彻,我们才能避免一些错误地理解。Java 中一些高级的特性,也建立在 Java 内存模型的基础上,例如:volatile 关键字。为了让大家能明白 Java 内存模型存在的意义,本篇文章将从计算机硬件出发,一路写到操作系统、编程语言,一环扣一环的引出 Java 内存模型存在的意义,让大家对 Java 内存模型有较为深刻的理解。看完之后,希望大家能够明白如下几个问题:
我们知道计算机有 CPU 和内存两个东西,CPU 负责计算,内存负责存储数据,每次 CPU 计算前都需要从内存获取数据。我们知道 CPU 的运行速度远远快于内存的速度,因此会出现 CPU 等待内存读取数据的情况。
由于两者的速度差距实在太大,我们为了加快运行速度,于是计算机的设计者在 CPU 中加了一个CPU 高速缓存。这个 CPU 高速缓存的速度介于 CPU 与内存之间,每次需要读取数据的时候,先从内存读取到CPU缓存中,CPU再从CPU缓存中读取。这样虽然还是存在速度差异,但至少不像之前差距那么大了。
随着技术的发展,多核 CPU 出现了,CPU 的计算能力进一步提高。原本同一时间只能运行一个任务,但现在可以同时运行多个任务。由于多核 CPU 的出现,虽然提高了 CPU 的处理速度,但也带来了新的问题:缓存一致性。
在多 CPU 系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存,如下图所示。当多个 CPU 的运算任务都涉及同一块主内存区域时,可能导致各自的缓存数据不一致。如果发生了这种情况,那同步回主内存时以哪个 CPU 高速缓存的数据为准呢?
我们举个例子,线程 A 执行这样一段代码:
i = i + 10;
线程 B 执行这样一段代码:
i = i + 10;
他们的 i 都是存储在内存中共用的,初始值是 0。按照我们的设想,最终输出的值应该是 20 才对。但实际上有可能输出的值是 10。下面是可能发生的一种情况:
可以看到发生错误结果的主要原因是:两个 CPU 高速缓存中的数据是相互独立,它们无法感知到对方的变化。
到这里,就产生了第一个问题:硬件层面上,由于多 CPU 的存在,以及加入 CPU 高速缓存,导致的数据一致性问题。
要注意的是,这个问题是硬件层面上的问题。只要使用了多 CPU 并且 CPU 有高速缓存,那就会遇到这个问题。对于生产该 CPU 的厂商,就需要去解决这个问题,这与具体操作系统无关,也与编程语言无关。
那么如何解决这个问题呢?答案是:缓存一致性协议。
所谓的缓存一致性协议,指的是在 CPU 高速缓存与主内存交互的时候,遵守特定的规则,这样就可以避免数据一致性问题了。
在不同的 CPU 中,会使用不同的缓存一致性协议。例如 MESI 协议用于奔腾系列的 CPU 中,而 MOSEI 协议则用于 AMD 系列 CPU 中,Intel 的 core i7 处理器使用 MESIF 协议。在这里我们介绍最为常见的一种:MESI数据一致性协议。
在 MESI 协议中,每个缓存可能有有4个状态,它们分别是:
那么在 MESI 协议的作用下,我们上面的线程执行过程就变为:
从上面的例子,我们可以知道 MESI 缓存一致性协议,本质上是定义了一些内存状态,然后通过消息的方式通知其他 CPU 高速缓存,从而解决了数据一致性的问题。
操作系统,它屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化,方便我们进行上层软件的开发。在我们开发应用软件的时候,我们不需要直接与硬件进行交互,只需要和操作系统交互即可。既然如此,那么操作系统就需要将硬件进行封装,然后抽象出一些概念,方便上层应用使用。于是 CPU 时间片、内核态、用户态等概念也诞生了。
前面我们说到 CPU 与内存之间会存在缓存一致性问题,那操作系统抽象出来的 CPU 与内存也会面临这样的问题。因此,操作系统层面也需要去解决同样的问题。所以,对于任何一个系统来说,它们都需要去解决这样一个问题。我们把在特定的操作协议下,对特定内存或高速缓存进行读写访问的过程进行抽象,得到的就是内存模型了。 无论是 Windows 系统,还是 Linux 系统,它们都有特定的内存模型。
Java 语言是建立在操作系统上层的高级语言,它只能与操作系统进行交互,而不与硬件进行交互。与操作系统相对于硬件类似,操作系统需要抽象出内存模型,那么 Java 语言也需要抽象出相对于操作系统的内存模型。一般来说,编程语言也可以直接复用操作系统层面的内存模型,例如:C++ 语言就是这么做的。但由于不同操作系统的内存模型不同,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错。因此在某些场景下,就必须针对不同的平台来编写程序。
而我们都知道 Java 的最大特点是「Write Once, Run Anywhere」,即一次编译哪里都可以运行。而为了达到这样一个目标,Java 语言就必须在各个操作系统的基础上进一步抽象,建立起一套对内存或高速缓存的读写访问抽象标准。这样就可以保证无论在哪个操作系统,只要遵循了这个规范,都能保证并发访问是正常的。
经过了前面的铺垫,相信你已经明白了为什么要有 Java 内存模型,以及 Java 内存模型是什么,有了一个感性的理解。这里我们再给 Java 内存模型下一个较为准确的定义。
Java 内存模型(Java Memory Model,JMM)用于屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台都能达到一致的内存访问效果。
Java 内存模型定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里说的变量包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数。因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
Java 内存模型规定所有的变量都存储在主内存中,每条线程都有自己的工作内存。线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递都需要通过主内存来完成。主内存、工作内存、线程三者之间的关系如下图所示。
Java 内存模型的主内存、工作内存与 JVM 的堆、栈、方法区,并不是同一层次的内存划分,两者是没有关联的。如果一定要对应一下,那么主内存主要对应于 Java 堆中对象实例的数据部分,而工作内存则对应于虚拟机栈中的部分区域。
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,以及如何从工作内存同步回主内存的细节,Java 内存模型定义了 8 种操作来完成。虚拟机实现的时候必须保证下面提及的每一种操作都是原子的、不可再分的。
如果要把一个变量从主内存复制到工作内存,那就要顺序地执行 read 和 load 操作,如果要把变量从工作内存同步回主内存,就要顺序地执行 store 和 write 操作。注意,Java 内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read 与 load 之间、store 与 write 之间是可插入其他指令的,如对主内存中的变量 a、b 进行访问时,一种可能出现顺序是 read a、read b、load b、load a。
此外,Java 内存模型还规定上述 8 种基本操作时必须满足如下规则:
这 8 种内存访问操作以及上述规则限定,再加上稍后介绍的对 volatile 的一些特殊规定,就已经完全确定了 Java 程序中哪些内存访问操作在并发下是安全的。 看完了 Java 内存模型的 8 个基本操作和 8 个规则,感觉太过于繁琐了,非常不利于我们日常代码的编写。为了能帮助编程人员理解,于是就有了与其相等价的判断原则 —— 先行发生原则,它可以用于判断一个访问在并发环境下是否安全。
这篇文章我们从底层 CPU 开始讲起,一直讲到操作系统,最后讲到了编程语言层面,让大家能够一环扣一环地理解,最后明白 Java 内存模型诞生的原因(上层有数据一致性问题),以及最终要解决的问题(缓存一致性问题)。看到这里,我们大概把为什么要有 Java 内存模型讲清楚了,也知道了 Java 内存模型是什么。最后我们来做个总结:
如果 Java 程序能够遵守 Java 内存模型的规则,那么其写出的程序就是并发安全的,这就是 Java 内存模型最大的价值。
来源:
https://www.cnblogs.com/chanshuyi/p/deep-insight-of-java-memory-model.html
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!