Loading...
墨滴

江璇

2021/05/21  阅读:47  主题:科技蓝

聊聊 Java 内存模型

作为一名后端开发,能够深入理解并发编程的精髓,是一件非常有必要的事情,今天我们从 Java 内存模型开始浅谈一下并发原理。

Java 内存模型

在介绍 Java 内存模型之前,我们先来看一下什么是计算机内存模型吧

大家都知道程序在运行过程中,每条指令都是由 CPU 执行的,而在执行过程中的数据都是存放在计算机的主存,也就是物理内存中。一开始 CPU 技术不发达,内存还是能够跟 CPU 的速度匹配,但是后面随着 CPU 发展的越来越快,内存的的速度已经满足不了 CPU 的需求了,此时,内存速度成了制约程序运行速度的核心问题。解决办法就是在 CPU 和 内存之间增加高速缓存(Cache),将 CPU 运算所需要的数据从主存复制一份到 高速缓存中, CPU 在执行程序时,直接从缓存中读取数据,并将数据写回到缓存中,等到运算结束后,再将缓存中的数据刷新回到主存中。

CPU 技术已经越来越成熟了,现在 CPU 的配置都是多核,三级缓存起步。当 CPU 要命中一个数据时,首先会从一级缓存中查找,如果没有命中那么就会从二级缓存中查找,如果还是没有命中的话,那么才会到三级缓存和内存中去查找。如图所示:

图1:x86架构下CPU缓存架构图
图1:x86架构下CPU缓存架构图

在 CPU 中存在“缓存一致性协议”,所以多个 CPU 之间的缓存不会出现不同步的问题,不会有“内存可见性”问题。但是,缓存一致性问题,对性能有很大的消耗,所以 CPU 设计者们又对这个架构进行优化,在 CPU 的计算单元和 L1 之间增加了 Store Buffer、Load Buffer(还有其他各种 Buffer),如图所示。

因为有缓存一致性协议的存在,L1、L2、L3和主内存之间是同步的,但是 Store Buffer、 Load Buffer 和 L1 之间是异步的,通常往内存中写入一个变量,这个变量会保存在 Store Buffer 里面,然后才会异步的写入到 L1 中,同时同步写入主内存中。

不过从操作系统内核的角度分析,我们可以将整个 CPU 缓存模型简化成下图:

每个逻辑 CPU 都有自己缓存,这些缓存和主内存之间不是完全同步的,对应到 Java 里,就是 JVM 抽象的内存模型(JMM)。如下图所示:

内存可见性

上面在介绍“缓存一致性”的时候提到过一个词 “内存可见性” 。

那什么又是内存可见性呢?

一般并发环境中,至少存在两个线程对同一个资源进行操作,如果B线程不能在A线程对资源的修改的第一时刻感知,那么它就不是内存可见,但是之后就能够满足可见。其实这个是满足“最终一致性”,而不是“修改后的第一时刻强一致性”。

所以一般我们常说的“内存可见性”指的是“写完之后立即对其他线程可见”,它的反面不是“不可见”,而是“稍后才能可见”。

使用 volatile 关键字就能够有效的解决这个问题。

重排序

Store Buffer 的延迟(异步)写入是重排序的一种,称为内存重排序。除此之外,还有编译器和 CPU 的指令重排序

  1. 编译器重排序,对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序。
  2. CPU 指令重排序,在指令级别,让没有依赖关系的多条指令并行。
  3. CPU 内存重排序,CPU 有自己的缓存,指令的执行顺序和写入顺序不完全一致。

导致“内存可见性”问题的主要原因的就是第三种。

重排序发生的规则

一般我们作为开发者都是比较关注线程安全问题,希望系统程序能够按照自己编程实现的思路正确的运行,不希望程序出现任何的重排序,这样非常方便理解,指令执行顺序和代码顺序保持一致,写内存的顺序也跟代码的书写顺序保持一致。

但是,从编译器和 CPU 的角度来看,希望尽最大可能进行重排序,这样可以提升运行效率

如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。那么重排序的原则是什么?什么场景下希望可以重排序?什么场景下是不能重排序的?这些都是值得我们深入思考的问题。

1. 单线程程序的重排序规则

无论什么语言,站在编译器和 CPU 的角度来说,不管怎么重排序,单线程程序的执行结果不能改变,这就是单线程程序的重排序规则(as-if-serial 语义)。意思就是:只要操作之间没有数据依赖,那么编译器和 CPU 都可以任意重排序,因为执行结果不会改变,代码就像是完全串行地一行一行从头执行到尾,这也就是 as-if-serial 语义。

2. 多线程程序的重排序规则

对于多线程来说,线程之间的数据依赖性太复杂了,编译器和 CPU 没有办法完全理解这种依赖性并据此做出最合理的优化,所以,编译器和 CPU 只能保证每个线程在满足单线程程序重排序规则(as-if-serial 语义)。线程之间的数据依赖和相互影响需要编译器和 CPU 的上层来确定。上层要告知编译器和 CPU 在多线程场景下什么时候可以重排序,什么时候不能重排序。

如何保证正确的重排序

为了明确定义在多线程下,什么时候可以重排序,什么时候不能重排序。Java 引入了 JMM(Java Memory Model)也就是 Java 内存模型。Java 内存模型其实就是一整套规范,对上,是 JVM 和开发者之间的协定;对下,是 JVM 和编译器、CPU 之间的协定。

之所以定义这套规范,就是为了让开发者在编写程序时在方便性和系统运行效率之间找到一个平衡点。一方面,要让编译器和 CPU 能够灵活地重排序;另一方面,为了让开发者明确这样编写程序是否会因为重排序而受到影响,受到的这种影响是否应该被禁止,如果有影响,那么就需要开发者显式地通过 volatile、synchronized 等线程同步机制来禁止重排序。

其实整个 Java 内存模型是围绕着并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。通常我们都是使用 volatile 和 synchronized 来保证,如下图所示:

happen-before 原则

如果 Java 内存模型中所有的有序性都是靠 volatile 和 synchronized 来实现,那么编写程序的过程就会非常不简洁。

因此,JMM 引入了 happen-before原则,下面这些 “天然的” happen-before 关系无需任何同步器协助就已经存在,可以在编码中直接使用,如果两个操作之间的关系不在此列,并且无法从以下规则中推导出来,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。

  • 程序次序规则(Program Order Rule):在一个线程中,按照控制流程顺序,书写在前面的操作先行发生于书写在后面的操作。注意这里说的是控制流顺序,而不是程序的代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是“同一个锁”,而“后面” 指的是时间上的先后。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于此线程的终止检测,我们可以通过 Thread 的 join () 方法是否结束以及 Thread 的 isAlive() 方法的返回值等手段检测线程是否已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程的 interrupt() 方法调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过 Thread 的 interrupted() 方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B ,操作 B 先行发生于操作 C ,那么可以得出操作 A 先行发生于操作 C 。

时间先后顺序与 happen-before 原则之间基本没有因果关系,所以在衡量并发安全问题的时候不要受时间顺序的干扰,一些必须要以先行发生原则为准。

后话

本次针对 Java 内存模型进行了简单概述,很多知识点都是参考了《Java并发实现原理 | JDK源码剖析》和《深入理解Java虚拟机 | JVM高级特性与最佳实践》两本书,通过拜读前辈的著作,让我领悟到 Java 虚拟机设计者的内功之深厚,同时也深深明白了自己实力的差距。

给自己立下一个 flag ,以后每个月至少要看一本经典著作,书籍都是大佬们产出的精华,而我们只需要花费几十块钱就能够与大佬们进行思想交流,实在是性价比最高的一种知识获取方式了。

保持阅读,保持开放共享的心态,永远不要停下学习的脚步。

我非常认同一句话:“你无法获取你认知范畴以外的财富”,而提高认知能力的方法就是不断学习,不断阅读,不断和各个行业的人交朋友,互相分享,互相进步,我是江璇,一个即将踏入职场的新人程序员,绿厂 Java 后端开发,期待和你们交个朋友,一起分享进步的经验,如果你也认可我,欢迎给我点个关注,在看。公众号菜单加我好友,先从朋友圈点赞之交开始吧!

目前江璇的技术交流群里已经有不少小伙伴了,里面有 BAT 一线大厂开发,CSDN 博客专家,ICPC国奖获得者,前面这些都是我的同学和校友们,还有在网络上认识优秀的小伙伴们,跟优秀的人在一起交流,你也会越来越优秀。欢迎公众号后台菜单加技术交流群。

江璇

2021/05/21  阅读:47  主题:科技蓝

作者介绍

江璇

专注Java后端技术,分享生活。