已经有 MESI 协议,为什么还需要 volatile 关键字?

news2025/7/19 21:01:50

本文已收录到  GitHub · AndroidFamily,有 Android 进阶知识体系,欢迎 Star。技术和职场问题,请关注公众号 [彭旭锐] 进 Android 面试交流群。

前言

大家好,我是小彭。

在上一篇文章里,我们聊到了 CPU 的缓存一致性问题,分为纵向的 Cache 与内存的一致性问题以及横向的多个核心 Cache 的一致性问题。我们也讨论了 MESI 协议通过写传播和事务串行化实现缓存一致性。

不知道你是不是跟我一样,在学习 MESI 协议的时候,自然地产生了一个疑问:在不考虑写缓冲区和失效队列的影响下,在硬件层面已经实现了缓存一致性,那么在 Java 语言层面为什么还需要定义 volatile 关键字呢?是多此一举吗?今天我们将围绕这些问题展开。


学习路线图:


1. 回顾 MESI 缓存一致性协议

由于 CPU 和内存的速度差距太大,为了拉平两者的速度差,现代计算机会在两者之间插入一块速度比内存更快的高速缓存,CPU 缓存是分级的,有 L1 / L2 / L3 三级缓存。其中 L1 / L2 缓存是核心独占的,而 L3 缓存是多核心共享的。

在 CPU Cache 的三级缓存中,会存在 2 个缓存一致性问题:

  • 纵向 - Cache 与内存的一致性问题:通过写直达或写回策略解决;
  • 横向 - 多核心 Cache 的一致性问题:通过 MESI 等缓存一致性协议解决。

MESI 协议能够满足写传播和事务串行化 2 点特性,通过 “已修改、独占、共享、已失效” 4 个状态实现了 CPU Cache 的一致性;

现代 CPU 为了提高并行度,会在增加写缓冲区 & 失效队列将 MESI 协议的请求异步化,这其实是一种处理器级别的指令重排,会破坏了 CPU Cache 的一致性。

Cache 不一致问题

MESI 协议在线模拟

网站地址:https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESI.htm

现在,我们的问题是:既然 CPU 已经实现了 MESI 协议,为什么 Java 语言层面还需要定义 volatile 关键字呢?岂不是多此一举?你可能会说因为写缓冲区和失效队列破坏了 Cache 一致性。好,那不考虑这个因素的话,还需要定义 volatile 关键字吗?

其实,MESI 解决数据一致性(Data Conherence)问题,而 volatile 解决顺序一致性(Sequential Consistency)问题。 WC,这两个不一样吗?


2. 数据一致性 vs 顺序一致性

2.1 数据一致性

数据一致性讨论的是同一份数据在多个副本之间的一致性问题, 你也可以理解为多个副本的状态一致性问题。例如内存与多核心 Cache 副本之间的一致性,或者数据在主从数据库之间的一致性。

当我们从 CPU 缓存一致性问题开始,逐渐讨论到 Cache 到内存的写直达和写回策略,再讨论到 MESI 等缓存一致性协议,从始至终我们讨论的都是 CPU 缓存的 “数据一致性” 问题,只是为了简便我们从没有刻意强调 “数据” 的概念。

数据一致性有强弱之分:

  • 强数据一致性: 保证在任意时刻任意副本上的同一份数据都是相同的,或者允许不同,但是每次使用前都要刷新确保数据一致,所以最终还是一致。
  • 弱数据一致性: 不保证在任意时刻任意副本上的同一份数据都是相同的,也不要求使用前刷新,但是随着时间的迁移,不同副本上的同一份数据总是向趋同的方向变化,最终还是趋向一致。

例如,MESI 协议就是强数据一致性的,但引入写缓冲区或失效队列后就变成弱数据一致性,随着缓冲区和失效队列被消费,各个核心 Cache 最终还是会趋向一致状态。

2.2 顺序一致性

顺序一致性讨论的是对多个数据的多次操作顺序在整个系统上的一致性。在并发编程中,存在 3 种指令顺序:

  • 编码顺序(Progrom Order): 指源码中指令的编写顺序,是程序员视角看到的指令顺序,不一定是实际执行的顺序;
  • 执行顺序(Memory Order): 指单个线程或处理器上实际执行的指令顺序;
  • 全局执行顺序(Global Memory Order): 每个线程或处理器上看到的系统整体的指令顺序,在弱顺序一致性模型下,每个线程看到的全局执行顺序可能是不同的。

顺序一致性模型是计算机科学家提出的一种理想参考模型,为程序员描述了一个极强的全局执行顺序一致性,由 2 个特性组成:

  • 特性 1 - 执行顺序与编码顺序一致: 保证每个线程中指令的执行顺序与编码顺序一致;
  • 特性 2 - 全局执行顺序一致: 保证每个指令的结果会同步到主内存和各个线程的工作内存上,使得每个线程上看到的全局执行顺序一致。

举个例子,线程 A 和线程 B 并发执行,线程 A 执行 A1 → A2 → A3,线程 B 执行 B1 → B2 → B3。那么,在顺序一致性内存模型下,虽然程序整体执行顺序是不确定的,但是线程 A 和线程 B 总会按照 1 → 2 → 3 编码顺序执行,而且两个线程总能看到相同的全局执行顺序。

顺序一致性内存模型

2.3 弱顺序一致性(一定要理解)

虽然顺序一致性模型对程序员非常友好,但是对编译器和处理器却不见得喜闻乐见。如果程序完全按照顺序一致性模型来实现,那么处理器和编译器的很多重排序优化都要被禁止,这对程序的 “并行度” 会有影响。例如:

  • 1、重排序问题: 编译器和处理器不能重排列没有依赖关系的指令;
  • 2、内存可见性问题: CPU 不能使用写回策略,也不能使用写缓冲区和失效队列机制。其实,从内存的视角看也是指令重排问题。

所以,在 Java 虚拟机和处理器实现中,实际上使用的是弱顺序一致性模型:

  • 特性 1 - 不要求执行顺序与编码顺序一致: 不要求单线程的执行顺序与编码顺序一致,只要求执行结果与强顺序执行的结果一致,而指令是否真的按编码顺序执行并不关心。因为结果不变,从程序员的视角看程序就是按编码顺序执行的假象;
  • 特性 2 - 不要求全局执行顺序一致: 允许每个线程看到的全局执行顺序不一致,甚至允许看不到其他线程已执行指令的结果。

举个单线程的例子: 在这段计算圆面积的代码中,在弱顺序一致性模型下,指令 A 和 指令 B 可以不按编码顺序执行。因为 A 和 B 没有数据依赖,所以对最终的结果也没有影响。但是 C 对 A 和 B 都有数据依赖,所以 C 不能重排列到 A 或 B 的前面,否则会改变程序结果。

伪代码

double pi = 3.14; // A
double r = 1.0// B
double area = pi * r * r; // C(数据依赖于 A 和 B,不能重排列到前面执行)

指令重排

再举个多线程的例子: 我们在 ChangeThread 线程修改变量,在主线程观察变量的值。在弱顺序一致性模型下,允许 ChangeThread 线程 A 指令的执行结果不及时同步到主线程,在主线程看来就像没执行过 A 指令。

这个问题我们一般会理解为内存可见性问题,其实我们可以统一理解为顺序一致性问题。 主线程看不到 ChangeThread 线程 A 指令的执行结果,就好像两个线程看到的全局执行顺序不一致:ChangeThread 线程看到的全局执行顺序是:[B],而主线程看到的全局执行顺序是 []。

可见性示例程序

public class VisibilityTest {
    public static void main(String[] args) {
        ChangeThread thread = new ChangeThread();
        thread.start();
        while (true) {
            if (thread.flag) { // B
                System.out.println("Finished");
                return;
            }
        }
    }

    public static class ChangeThread extends Thread {
        private boolean flag = false;

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true; // A
            System.out.println("Change flag = " + flag);
        }
    }
}

程序输出

Change flag = true
// 无限等待

前面你说到编译器和处理器的重排序,为什么指令可以重排序,为什么重排序可以提升性能,重排序不会出错吗?


3. 什么是指令重排序?

3.1 重排序类型

从源码到指令执行一共有 3 种级别重排序:

  • 1、编译器重排序: 例如将循环内重复调用的操作提前到循环外执行;
  • 2、处理器系统重排序: 例如指令并行技术将多条指令重叠执行,或者使用分支预测技术提前执行分支的指令,并把计算结果放到重排列缓冲区(Reorder Buffer)的硬件缓存中,当程序真的进入分支后直接使用缓存中的结算结果;
  • 3、存储器系统重排序: 例如写缓冲区和失效队列机制,即是可见性问题,从内存的角度也是指令重排问题。

指令重排序类型

3.2 什么是数据依赖性?

编译器和处理器在重排序时,会遵循数据依赖性原则,不会试图改变存在数据依赖关系的指令顺序。如果两个操作都是访问同一个数据,并且其中一个是写操作,那么这两个操作就存在数据依赖性。此时一旦改变顺序,程序最终的执行结果一定会发生改变。

数据依赖性分为 3 种类型::

数据依赖性描述示例
写后读写一个数据,再读这个数据a = 1; // 写
b = a; // 读
写后写写一个数据,再写这个数据a = 1; // 写
a = 2; // 写
读后写读一个数据,再写这个数据b = a; // 读
a = 1; // 写

3.3 指令重排序安全吗?

需要注意的是:数据依赖性原则只对单个处理器或单个线程有效,因此即使在单个线程或处理器上遵循数据依赖性原则,在多处理器或者多线程中依然有可能改变程序的执行结果。

举例说明吧。

例子 1 - 写缓冲区和失效队列的重排序: 如果是在一个处理器上执行 “写后读”,处理器不会重排这两个操作的顺序;但如果是在一个处理器上写,之后在另一个处理器上读,就有可能重排序。关于写缓冲区和失效队列引起的重排序问题,上一篇文章已经解释过,不再重复。

写缓冲区造成指令重排

例子 2 - 未同步的多线程程序中的指令重排: 在未同步的两个线程 A 和 线程 B 上分别执行这两段程序,程序的预期结果应该是 4,但实际的结果可能是 0

线程 A

a = 2; // A1
flag = true; // A2

线程 B

while (flag) { // B1
    return a * a; // B2
}

情况 1:由于 A1 和 A2 没有数据依赖性,所以编译器或处理器可能会重排序 A1 和 A2 的顺序。在 A2 将 flag 改为 true 后,B1 读取到 flag 条件为真,并且进入分支计算 B2 结果,但 A1 还未写入,计算结果是 0。此时,程序的运行结果就被重排列破坏了。

情况 2:另一种可能,由于 B1 和 B2 没有数据依赖性,CPU 可能用分支预测技术提前执行 B2,但 A1 还未写入,计算结果还是 0。此时,程序的运行结果就被重排列破坏了。

多线程的数据依赖性不被考虑

小结一下: 重排序在单线程程序下是安全的(与预期一致),但在多线程程序下是不安全的。


4. 回答最初的问题

到这里,虽然我们的讨论还未结束,但已经足够回答标题的问题:“已经有 MESI 协议,为什么还需要 volatile 关键字?”

即使不考虑写缓冲区或失效队列,MESI 也只是解决数据一致性问题,并不能解决顺序一致性问题。在实际的计算机系统中,为了提高程序的性能,Java 虚拟机和处理器会使用弱顺序一致性模型。

在单线程程序下,弱顺序一致性与强顺序一致性的执行结果完全相同。但在多线程程序下,重排序问题和可见性问题会导致各个线程看到的全局执行顺序不一致,使得程序的执行结果与预期不一致。

为了纠正弱顺序一致性的影响,编译器和处理器都提供了 “内存屏障指令” 来保证程序关键节点的执行顺序能够与程序员的预期一致。在高级语言中,我们不会直接使用内存屏障,而是使用更高级的语法,即 synchronized、volatile、final、CAS 等语法。

那么,什么是内存屏障?synchronized、volatile、final、CAS 等语法和内存屏障有什么关联,这个问题我们在下一篇文章展开讨论,请关注。


参考资料

  • Java 并发编程的艺术(第 1、2、3 章) —— 方腾飞 魏鹏 程晓明 著
  • 深入理解 Android:Java 虚拟机 ART(第 12.4 节) —— 邓凡平 著
  • 深入理解 Java 虚拟机(第 5 部分) —— 周志明 著
  • 深入浅出计算机组成原理(第 55 讲) —— 徐文浩 著,极客时间 出品
  • CPU有缓存一致性协议(MESI),为何还需要 volatile —— 一角钱技术 著
  • 一文读懂 Java 内存模型(JMM)及 volatile 关键字 —— 一角钱技术 著
  • MESI protocol —— Wikipedia
  • Cache coherence —— Wikipedia
  • Sequential consistency —— Wikipedia
  • Out-of-order execution —— Wikipedia
  • std::memory_order —— cppreference.com

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CnQCpetn-1668800502326)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7db0e43b744943f685ad1e3627f1ceb1~tplv-k3u1fbpfcp-watermark.image?)]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/17324.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

树莓派使用docker搭建owncloud私有云--外挂硬盘

一.安装docker 1. 一键脚本: sudo curl -sSL https://get.docker.com | sh2. 查看docker是否安装成功 docker -v出现版本号即为成功 二.每次开机自动挂载硬盘到树莓派 sudo nano /etc/fstab在最后一行加入挂载信息 /dev/sda1 /home/pi/…

[附源码]java毕业设计农村政务管理系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

[附源码]SSM计算机毕业设计智慧农业销售平台JAVA

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

多进程编程 VS 多线程编程

目录 一、进程 & 线程 二、进程与线程的优劣势 三、在什么场景下需要使用多进程编程? 进程也可以称为是“任务”。操作系统要想执行一个具体的“动作”,就需要创建出一个对应的进程。 一个程序在没有运行的时候,它仅仅是一个“可执行…

RHCE学习 --- 第六次作业

RHCE学习 — 第六次作业 首先要先装DNS服务器需要的包 [rootlocalhost ~]# yum install bind -y然后开始配置DNS服务 配置文件位置在/etc/named.conf下,建议先备份 注:备份的时候要cp -a,否则所属组会变,导致文件不可用 然后编辑…

WinForm,可能是Windows上手最快的图形框架了

文章目录Label和控件属性按钮和回调逻辑事件常用控件Label和控件属性 WinForm是一门非常经济实惠的技术,就是说,可以在短时间内学会,并迅速借此进行项目开发。尽管在很多方面不够现代,做出来的东西又Low又丑,但绝大多…

Redis的优惠券秒杀问题(六)超卖问题、一人一单问题

Redis的优惠券秒杀问题(六)超卖问题、一人一单问题 超卖问题 问题描述 使用Jmeter进行压测 发生超卖问题原因分析 解决方案 悲观锁与乐观锁 1. 版本号 2. CAS法 CAS三大问题(题外话!) CAS三大问题的解…

误差和梯度下降

Datawhale开源学习,机器学习课程,项目地址:https://github.com/datawhalechina/leeml-notes 之前讲了线性模型,提到了误差,那么误差来自哪里?本节内容将介绍「偏差」、「方差」对模型拟合度的影响&#xff…

西电计组II 实验1

西电计组II 实验1 文章目录西电计组II 实验18086汇编 IO操作环境搭建8086汇编 helloworldassumesegmentdb编译链接lstmapobjexesymdobint 21H 软件中断程序设计要求全局变量函数设计putchargetcharprintnewlineinputmemsetexithexbinarycircle程序入口完整代码8086汇编 IO操作 …

wav to image 的数据集制作代码

🍿*★,*:.☆欢迎您/$:*.★* 🍿 目录 背景 正文 总结 背景描述

python+django网吧会员管理系统

系统项目截图 本网吧管理系统主要包括三大功能模块,即管理员、会员、网管。 (1)管理员模块:首页、个人中心、会员管理、网管管理、商品类型管理、商品信息管理、购买商品信息管理、呼叫网管管理、电脑信息管理、用户上机管理、用户…

汇编语言外中断

外中断 文章目录外中断1.外中断概念2.PC机键盘的处理过程1.外中断概念 CPU在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出(I/O能力&#xff0…

如何把家装修出温馨的感觉?极家好不好

如何把家装修出温馨的感觉?极家好不好?想要让家变成理想的样子!如何进行! 第一步:找一个靠谱的装修团队,特别重要的是项目经理,极家好不好这个真的真的真的很重要‼️ 一个好的装修团队&#xf…

Windbg可以看到Visual Studio中看不到的有效函数调用堆栈

目录 1、Visual Studio中看不到有效的函数调用堆栈 2、使用Windbg调试运行主程序,看到了有效的函数调用堆栈 3、根据函数名和行号去查看对应的C源码,定位问题 4、总结 VC常用功能开发汇总(专栏文章列表,欢迎订阅,持…

Frechet distance距离计算原理及python实现

Frechet distance概念 弗雷彻距离(Frechet distance)定义了一种考虑位置和点的次序的计算两条曲线相似度的方法,常用于时间序列相似度度量和轨迹序列相似度度量。该指标的算法可用the walking dog problem来描述:想象一个人用牵引带遛狗的同时&#xff0…

“碳”零排放是什么意思

在气候变化问题上拖拖拉拉之后,澳大利亚联邦政府终于承诺到 2050 年实现净零排放,世界其他大部分地区也是如此。几乎所有发达经济体现在都加强了 2030 年目标,并承诺在本十年将排放量大致减半。 需要记住的重要一点是,如果没有本…

abbyy finereader2023泰比文字识别PDF编辑软件

近年来,随着盲人数字阅读的普及推广,PDF格式的电子书越来越受到大家的关注和喜爱,但受读屏软件功能的限制,扫描版的PDF电子书是无法直接阅读的,这就需要将其转换为可阅读的文档格式,可对于大多数视障读者来…

Linux基本指令1

系统内核:Centos 7.6 64位操作系统(OS, operating system)是什么?世界上第一台计算机诞生的时候是没有操作系统的,但是这个计算机操作起来效率特别低,难度非常高。使用对象只有科学家。操作系统的意义就在于降低操作难度&#…

数据库高级 V

数据库高级 V 1.JVM内存结构,JVM调优,GC常用算法 如何调整堆内存大小,以及调整各年代之间的比例,更换GC 修改JVM堆大小方式: 找到Idea安装目录下的-->bin-->idea.exe.vmoptions -server -Xms128m //堆初始大小 -Xmx512m //最大堆内存 -XX:ReservedCodeCacheSize240m -XX…

[附源码]计算机毕业设计JAVA花卉销售管理系统

[附源码]计算机毕业设计JAVA花卉销售管理系统 项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybati…