从 CPU 说起,深入理解 Java 内存模型!

Java 内存模型,许多人会错误地理解成 JVM 的内存模型。但实际上,这两者是完全不同的东西。Java 内存模型定义了 Java 语言如何与内存进行交互,具体地说是 Java 语言运行时的变量,如何与我们的硬件内存进行交互的。而 JVM 内存模型,指的是 JVM 内存是如何划分的。

Java 内存模型是并发编程的基础,只有对 Java 内存模型理解较为透彻,我们才能避免一些错误地理解。Java 中一些高级的特性,也建立在 Java 内存模型的基础上,例如:volatile 关键字。

为了让大家能明白 Java 内存模型存在的意义,本篇文章将从计算机硬件出发,一路写到操作系统、编程语言,一环扣一环的引出 Java 内存模型存在的意义,让大家对 Java 内存模型有较为深刻的理解。看完之后,希望大家能够明白如下几个问题:

为什么要有 Java 内存模型?Java 内存模型解决了什么问题?Java 内存模型是怎样的一个东西?从 CPU 说起

我们知道计算机有 CPU 和内存两个东西,CPU 负责计算,内存负责存储数据,每次 CPU 计算前都需要从内存获取数据。我们知道 CPU 的运行速度远远快于内存的速度,因此会出现 CPU 等待内存读取数据的情况。

由于两者的速度差距实在太大,我们为了加快运行速度,于是计算机的设计者在 CPU 中加了一个 CPU 高速缓存。这个 CPU 高速缓存的速度介于 CPU 与内存之间,每次需要读取数据的时候,先从内存读取到 CPU 缓存中,CPU 再从 CPU 缓存中读取。这样虽然还是存在速度差异,但至少不像之前差距那么大了。

从 CPU 说起,深入理解 Java 内存模型!插图亿华云

新增 CPU 高速缓存

随着技术的发展,多核 CPU 出现了,CPU 的计算能力进一步提高。原本同一时间只能运行一个任务,但现在可以同时运行多个任务。由于多核 CPU 的出现,虽然提高了 CPU 的处理速度,但也带来了新的问题:缓存一致性。

在多 CPU 系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存,如下图所示。当多个 CPU 的运算任务都涉及同一块主内存区域时,可能导致各自的缓存数据不一致。如果发生了这种情况,那同步回主内存时以哪个 CPU 高速缓存的数据为准呢?

从 CPU 说起,深入理解 Java 内存模型!插图1亿华云

多核 CPU 及高速缓存导致的问题

我们举个例子,线程 A 执行这样一段代码:

i = i 10;

线程 B 执行这样一段代码:

i = i 10;

他们的 i 都是存储在内存中共用的,初始值是 0。按照我们的设想,最终输出的值应该是 20 才对。但实际上有可能输出的值是 10。下面是可能发生的一种情况:

线程 A 分配到 CPU0 执行,这时候读取 i 的值为 0,存到 CPU0 的高速缓存中。线程 B 分配到 CPU1 执行,这时候读取 i 的值为 0,存到 CPU1 的高速缓存中。CPU0 进行运算,得出结果 10,运算结束,写回内存,此时内存 i 的值为 10。CPU1 进行运算,得出结果 10,运算结束,写回内存,此时内存 i 的值为 10。

可以看到发生错误结果的主要原因是:两个 CPU 高速缓存中的数据是相互独立,它们无法感知到对方的变化。

到这里,就产生了第一个问题:硬件层面上,由于多 CPU 的存在,以及加入 CPU 高速缓存,导致的数据一致性问题。

要注意的是,这个问题是硬件层面上的问题。只要使用了多 CPU 并且 CPU 有高速缓存,那就会遇到这个问题。对于生产该 CPU 的厂商,就需要去解决这个问题,这与具体操作系统无关,也与编程语言无关。

那么如何解决这个问题呢?答案是:缓存一致性协议。

从 CPU 说起,深入理解 Java 内存模型!插图2亿华云

加入缓存一致性协议

所谓的缓存一致性协议,指的是在 CPU 高速缓存与主内存交互的时候,遵守特定的规则,这样就可以避免数据一致性问题了。

在不同的 CPU 中,会使用不同的缓存一致性协议。例如 MESI 协议用于奔腾系列的 CPU 中,而 MOSEI 协议则用于 AMD 系列 CPU 中,Intel 的 core i7 处理器使用 MESIF 协议。在这里我们介绍最为常见的一种:MESI 数据一致性协议。

在 MESI 协议中,每个缓存可能有有 4 个状态,它们分别是:

M (Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中。E (Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本 Cache 中。S (Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多 Cache 中。I (Invalid):这行数据无效。

那么在 MESI 协议的作用下,我们上面的线程执行过程就变为:

线程 A 分配到 CPU0 执行,这时候读取 i 的值为 0,存到 CPU0 的高速缓存中。线程 B 分配到 CPU1 执行,这时候读取 i 的值为 0,存到 CPU1 的高速缓存中。CPU0 进行运算,得出结果 10,运算结束,写回内存,此时内存 i 的值为 10。同时通过消息的方式告诉其他持有 i 变量的 CPU 缓存,将这个缓存的状态值为 Invalid。CPU1 进行运算,从 CPU 缓存取出值,但是发现这个缓存值被置为 Invalid 了。于是重新去内存中读取,读取到 10 这个值放入 CPU 缓存。CPU1 进行运算,得出结果 20,运算结束,写回内存,此时内存 i 的值为 20。

从上面的例子,我们可以知道 MESI 缓存一致性协议,本质上是定义了一些内存状态,然后通过消息的方式通知其他 CPU 高速缓存,从而解决了数据一致性的问题。

从操作系统说起

操作系统,它屏蔽了底层硬件的操作细节,将各种硬件资源虚拟化,方便我们进行上层软件的开发。在我们开发应用软件的时候,我们不需要直接与硬件进行交互,只需要和操作系统交互即可。

既然如此,那么操作系统就需要将硬件进行封装,然后抽象出一些概念,方便上层应用使用。于是 CPU 时间片、内核态、用户态等概念也诞生了。

前面我们说到 CPU 与内存之间会存在缓存一致性问题,那操作系统抽象出来的 CPU 与内存也会面临这样的问题。因此,操作系统层面也需要去解决同样的问题。所以,对于任何一个系统来说,它们都需要去解决这样一个问题。

我们把在特定的操作协议下,对特定内存或高速缓存进行读写访问的过程进行抽象,得到的就是内存模型了。

THE END
Copyright © 2024 亿华云