Java 内存模型,或许应该这么理解


大家好,我是树哥。

在前面一段时间,我连续写了几篇关于并发编程的文章:

  • 从 CPU 讲起,深入理解 Java 内存模型! - 陈树义的博客
  • 深入理解 happens-before 原则 - 陈树义的博客
  • 深入理解 synchronized 的锁优化 - 陈树义的博客
  • 深入理解 Java 对象的内存布局 - 陈树义的博客
  • 深入理解 volatile 关键字 - 陈树义的博客

这几篇文章分别讲了 Java 内存模型、happens-before 原则、volatile 关键字、synchronized 关键字、Java 对象的内存布局。这 5 篇文章看着好像是独立的,但实际上他们是互相关联的。

在好几年前我也看过 Java 内存模型这些内容,但网上的内容实在太多太杂,始终找不到合理的解释。刚好就在几周前,当我再次认真看这些内容的时候,突然发现能比较好地串起来了,所以就写了这几篇文章。今天,就树哥一起与你一起重温下这几个知识点的联系与理解吧。

Java 内存模型

网上关于 Java 内存模型的内容特别多,很多都讲到了多 CPU 与缓存的数据一致性问题,于是顺带牵出了 MESI 等缓存一致性协议。其实到这里都没问题,都挺有逻辑的。

但接下来为啥有 Java 内存模型?为啥又有 happens-before 原则?这些内容基本上没有一个说得清楚,这就让人很困惑了。此外,有些还扯出了内存屏障、执行时序的问题,但都没啥逻辑,听起来乱糟糟的。我就曾专门花了一个晚上认真看某篇很火的文章,但最终也没搞懂。

对于 Java 内存模型,我舍弃了一些不必要的细碎点,整理了我的一些理解,我感觉相对来说还是比较好理解的。

首先,由于多核 CPU 和高速缓存在存在,导致了缓存一致性问题。 这个问题属于硬件层面上的问题,而解决办法是各种缓存一致性协议。不同 CPU 采用的协议不同,MESI 是最经典的一个缓存一致性协议。

其次,操作系统作为对底层硬件的抽象,自然也需要解决 CPU 高速缓存与内存之间的缓存一致性问题。 各个操作系统都对 CPU 高速缓存与缓存的读写访问过程进行抽象,最终得到的一个东西就是「内存模型」。

从硬件到操作系统,这个是我自己的理解,我并没有找到一些资料提到这点。但我觉得这应该是没有错的。因为操作系统就是对底层硬件的抽象,而所有抽象的东西就需要定义一些概念。

对于操作系统来说,这些概念就是内存模型、CPU 时间片等。内存模型这个词,在操作系统的教科书上也是可以找到的,这也是一个佐证吧。

于是,我们从硬件层面理解到了操作系统层面,但这跟 Java 内存模型有啥关系呢?

最后,Java 语言作为运行在操作系统层面的高级语言,为了解决多平台运行的问题,在操作系统基础上进一步抽象,得到了 Java 语言层面上的内存模型,其也是为了解决多线程情况下的数据一致性问题。

我们是因为要实现 Java 语言的「Write Once, Run Anywhere」的理念,那么就必须解决多平台内存模型不一致的问题,这样才创造出了 Java 内存模型。

Java 内存模型规定了很多规则,如果 Java 程序能够遵守 Java 内存模型的规则,那么其写出的程序就是并发安全的,这就是 Java 内存模型最大的价值。

到这里,我们从硬件、操作系统再到语言层面,知道了 Java 内存模型诞生的原因,知道其诞生就是为了解决多平台的内存模型统一问题,进一步其实就是多线程的数据一致性问题。

happens-before 原则

前面说到,为了解决多平台的内存模型统一,以及多线程的数据一致性问题,所以有了 Java 内存模型。但是 Java 内存模型的内容太多了,基本就记不住,非常不利于编程人员理解,所以才有了 happens-before 原则。

所以说 happens-before 原则是对 Java 内存模型的简化,让我们更好地写出并发代码。

volatile 关键字

volatile 关键字,其实也与 Java 内存模型有关系,只是很多文章都没说清楚。

volatile 关键字有两个作用,就是可见性和禁止指令重排序。但为啥它有这两个作用呢?其实 volatile 这两个作用的来源,就来自于 Java 内存模型里对 volatile 变量定义的特殊规则。

这就是 volatile 关键字与 Java 内存模型的关系,比较简单。

至于内存屏障这个词,其实就是一个让我们方便理解的名词,诞生于 volatile 禁止指令重排序这个作用里,也没啥不好理解的。

synchronized 关键字

synchronized 关键字,也是并发编程常用到的内容,其实它和 Java 内存模型没关系,但和 Java 虚拟机规范有关系。

synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码的执行需要指明一个要锁定或解锁的对象。而 monitorenter 和 monitorexit 这两个字节码指令为啥能实现这样的功能,是因为 Java 虚拟机中做了强制定义,那么虚拟机就需要实现。

synchronized 关键字与 Java 对象的内存布局,也是有关系的。自旋锁、自适应锁、偏向锁,它们靠什么实现,就是 Java 对象中的对象头去判断,然后进行一系列的逻辑操作。

总结

至此,我们基本上可以把 Java 并发编程里常见的那些概念的关系搞清楚了。

Java内存模型 是对内存布局的抽象,解决多平台运行以及多线程一致性的问题。happens-before 原则 是 Java 内存模型定义的简化,方便我们学习。volatile 则是轻量级同步同步机制,其来源于 Java 内存模型赋予的权利。

synchronized 关键字的合法性,则来自于 Java 虚拟机规范。而 synchronized 中自旋锁、自适应锁、偏向锁等,都依靠 Java 对象的对象头 来判断。

以上就是我对 Java 并发编程里常见概念的理解,感觉还是比较清晰一些。如果有什么理解得不对的,欢迎一起探讨探讨~