- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
**摘要: **本文并不讨论happends-before模型,只讨论底层原理,希望借着理解volatile,去理解一下它和CPU之间的关系
本文分享自华为云社区《当我深度探究了volatile的底层原理后,决定用超多的图解来一扫你对多线程问题本质的所有误区》,作者:breakDawn 。
《深入理解Java虚拟机》第二版中,关于volatile的原理,特地先讲述了如下图所示的JMM内存模型:
它用了8种内存操作,以及多种规则,来告诉你特定情况下线程间的数据会如何同步。
然而这个模型实际上已经在JDK5之后的虚拟机规范中被废弃了。
最新官方文档中是采用“happens-before”模型来帮助java程序员分析多线程环境下的运行情况。关于happends-before模型的详细解释,建议阅读《java并发编程的艺术》一书的第三章节。
本文并不讨论happends-before模型,只讨论底层原理,希望借着理解volatile,去理解一下它和CPU之间的关系。关于这部分内容,网上其实有很多错误的解读,根本原因在于没有从真正底层运行的原理考虑,导致了很多误区的产生。
本文将为你解答一下三大误区问题:
以上问题,我将从CPU的层面,以超长图解和文字的形式,为你完整呈现。
目录(社区的markdown功能不支持[toc]。。):
为什么需要这个JMM模型?用来做什么的?
上述模型和背后操作系统、CPU实现有什么关系?工作内存对应什么,主内存对应什么?
什么是CPU缓存?CPU缓存长什么样?
2个CPU同时更新数据时,有什么办法避免同时修改主存?
MESI协议超级详解
为什么要设计独占状态?
为什么CPU2对a值的获取,能够修改CPU1的状态?
修改完M状态后,发生了什么?是直接同步回内存吗?
不可见的误区例子
重头戏:有MESI保证可见性的下,为什么还有会多线程问题?
CPU1等待其他CPU清空缓存的阻塞等待行为会不会太慢了?
有什么办法能加快响应速度Invalid消息的响应速度呢?
多线程问题与StoreBuffer、Invalid-Queue之间的关系
volatile又是如何避免CPU更新延迟问题的?
图解总结
这是为了给java开发者提供屏蔽平台差异性的、统一的多线程执行语义
不同的操作系统或者不同的CPU,其对多线程并发问题的支持情况是不同的,但jvm尽量在背后将其实现成一套统一的逻辑。
即你按照我的关键字操作,就可以像JMM模型里那样运作,不需要关心背后的CPU怎么跑的
CPU缓存可以理解为一个容量有限的哈希表。
将某地址数据根据地址做哈希,映射到缓存的某一行,并且有替换的情况。而划分两列,则是避免一出现hash冲突,就马上淘汰原内容的情况。因此增加了备用列。通过这样一个多行两列的结构,根据内存地址实现了缓存的功能。
总线锁的代价是开销很大,其他CPU会一直在等待总线释放,读和写操作都无法处理。
画了很多张图,顺便带上很多文字,来解释MESI协议的原理:
首先,CPU1和CPU2中的缓存都是空的, 因此缓存状态位是“I”(Invalid),后面会相继提到各个状态的变化。
此时主存会将a的数据返回给CPU1, 并把CPU1的缓存状态设置为独占E(Exclude)
目的是为了减少不必要的全局同步消息。
当这个a变量只有CPU1使用时,无论CPU1怎么修改,也只有CPU1在查看,没必要把信息同步给其他CPU,从而提升了效率, 对于一些不参与竞争的变量来说,非常有用。
好,独占的问题搞定,那么当CPU2也读取时,会发生什么?
此时就会不再是独占状态了,2个CPU同时被修改为共享状态S(Share)
这是因为CPU可以通过总线广播+监听消息来变更状态, 也称嗅探机制。即CPU核心都会经常监听总线上的广播事件,根据事件(消息)类型,来做不同的应对。
因此当CPU2更改后,总线会广播read消息,当CPU1收到read消息,并确认这个数据的地址和自己缓存中的地址是一致的时候,就会修改状态为S了。
上述问题搞定,再看最关键的修改缓存的部分了!
结合下面的图进行阅读更佳,注意图中的序号:
很容易想到, 要将其他CPU设置为无效的原因,是为了保证其他CPU后面再次试图取a值时,取到的是最新的,而不是缓存的错误数据。
不是的!M状态此时就像“独占状态”一样,贪婪地占有这个缓存,后续的修改、读取,都直接读这个缓存,不再走任何总线
其目的和独占状态E一样,都是为了减少非竞争情况下不必要的总线消耗
那么什么时候Modify状态会变化呢?
当其他的CPU试图获取a值,就会发生变化。其过程与 Exclude独占状态到Share状态 是类似的
上面的内容给我一种感觉,MESI协议中,一直在给我们传达一个信息:MESI设置那么多状态,主要是为了避免每次都竞争。竞争只是偶然发生的,我们要尽可能少地乱锁总线!
从上面可以看到,当我们修改缓存时,会通过触发对其他缓存的无效化,达到变量对其他线程“可见”的效果。
因此,MESI缓存一致性协议已经实现了缓存的可见性,
下面这种例子中,当flag=true时, 其他CPU通过MESI协议,是能够感知到flag的变化的,因为缓存一定会在那个时刻被设置为无效,从而获取最新的数据。
class A {
static boolean flag = false;
static int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (!flag){
num++;
}
}).start();
flag = true;
}
//输出结果
1255362997
}
首先可以看这个知乎问题的回答,如果看不懂,可以看我为你整理的详细解释:
有了缓存一致性协议为什么还需要多线程同步? - 知乎
每个java线程有自己的寄存器。
线程寄存器和CPU缓存的关系?
上面的MESI协议图中,其实缺少了2个关键的优化,这2个优化点,也成了可见性问题的根源。
为了好好讲清楚这2个优化点带来的影响,我特地放到这里才讲述,也将会帮助你大大理解“可见性”问题的本质!
首先我们回到CPU1修改a值时的那张图:
里面提到,CPU1会等待所有CPU都将状态位置为I(无效)后,才开始修改状态并更新。那么有个问题:
如果CPU1后面有好几条和a无关的指令(例如b=3,d=e等),都在为了等待a的更新而不执行,未免太浪费时间了!
因此MESI设计了一个叫做 “StoreBuffer” 的东西,它会接收CPU1的修改动作,并由StoreBuffer来触发“阻塞等待->全部收到->修改状态M”的动作。
而CPU1则继续管自己去执行后续与a无关的指令。因此StoreBuffer就像是一个异步的“生产者消息队列”。,如下所示:
但是还有个问题,因为是等待所有CPU将a状态改为I,这个修改动作是需要时间的。
如果有一个CPU修改的比较慢,可能会导致 StoreBuffer这个生产者队列出现队满的情况,于是继续引发了阻塞。
那就是再引入一个“异步的消费者队列”,名叫Invalid Queue
这样其他CPU收到消息时,先别急着处理,而是存到这个Queue中,然后直接返回Invalid消息,这样响应就变快了! 也就是更新动作,和失效消息的接收,都加了一个队列!
如下图所示:
终于来到了关键的部分了。
从刚才的描述中,可以看到CPU引入了2个异步的队列,来处理数据的更新动作。
那么就可能存在赋值的动作被放入异步队列,导致延迟触发的情况。
而正是这个延迟放入的动作,可能导致数据延迟修改,即使没有发生指令重排序。
这样干讲比较难懂,还是需要结合代码和图解。
首先是这个经典的多线程代码:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
Public void reader() {
while (flag != true) { // 3
;
}
int c = a * 2; // 4
…………
}
}
按照设想,程序员本是希望有如下的表现:
但是事与愿违,当reader方法中离开了flag循环时,a的值仍然是初始化值0,导致c的值为0。
那么在了解了刚才的CPU原理后,我们终于可以开始分析这段代码为什么会发生这种问题了:
1、当线程writer执行a=1时,CPU要做更新,会通过上面提到的异步机制进行更新。如果这个CPU此时堆积了很多的写操作,会导致a=1这个动作在异步队列中处于等待。
2、时间片切换,线程writer切到了另一个CPU上
注意一个很重要的点:线程执行指令,并非只在1个CPU上运行,是可以通过时间片轮转切换的。因此CPU和线程并非完全绑定的关系
3、flag=true动作在CPU2上迅速响应,很快完成了缓存一致性
4、reader线程读到了最新的flag,却没有读到新的a,导致了a还在用旧的值。
因此可以看到,正是CPU之间缓存更新的延迟,导致了多线程不同步问题的发生
这里不谈论那让人费解的内存屏障, 只要记住一点:对于volatile变量,一旦更新,不会走CPU异步更新,而是在这个CPU阻塞住,直到写动作完整完成,才会继续下一个指令的运行
本质上是利用的#LOCK指令。
它的作用就是必须等待该变量的storeBuffer的清空,读取时也必须等待InvalidQueue的清空,才能去做写和读。从而保证不会出现因异步导致的多线程不同步问题
写文章不容易,学习也不容易,给我点个关注点个赞,未来会持续更新具有思考深度的学习文章。欢迎在华为云社区共同交流和学习。
MESI缓存一致性原理图解如下:
多线程同步问题如下:
我将 Bootstrap 与 css 和 java 脚本结合使用。在不影响前端代码的情况下,我真的很难在css中绘制这个背景。在许多问题中,人们将宽度和高度设置为 0%。但是由于我的导航栏,我不能使用
我正在用 c 编写一个程序来读取文件的内容。代码如下: #include void main() { char line[90]; while(scanf("%79[^\
我想使用 javascript 获取矩阵数组的所有对 Angular 线。假设输入输出如下: input = [ [1,2,3], [4,5,6], [7,8,9], ] output =
可以用pdfmake绘制lines,circles和other shapes吗?如果是,是否有documentation或样本?我想用jsPDF替换pdfmake。 最佳答案 是的,有可能。 pdfm
我有一个小svg小部件,其目的是显示角度列表(参见图片)。 现在,角度是线元素,仅具有笔触,没有填充。但是现在我想使用一种“内部填充”颜色和一种“笔触/边框”颜色。我猜想line元素不能解决这个问题,
我正在为带有三角对象的 3D 场景编写一个非常基本的光线转换器,一切都工作正常,直到我决定尝试从场景原点 (0/0/0) 以外的点转换光线。 但是,当我将光线原点更改为 (0/1/0) 时,相交测试突
这个问题已经有答案了: Why do people write "#!/usr/bin/env python" on the first line of a Python script? (22 个回
如何使用大约 50 个星号 * 并使用 for 循环绘制一条水平线?当我尝试这样做时,结果是垂直(而不是水平)列出 50 个星号。 public void drawAstline() { f
这是一个让球以对角线方式下降的 UI,但球保持静止;线程似乎无法正常工作。你能告诉我如何让球移动吗? 请下载一个球并更改目录,以便程序可以找到您的球的分配位置。没有必要下载足球场,但如果您愿意,也可以
我在我的一个项目中使用 Jmeter 和 Ant,当我们生成报告时,它会在报告中显示 URL、#Samples、失败、成功率、平均时间、最短时间、最长时间。 我也想在报告中包含 90% 的时间线。 现
我有一个不寻常的问题,希望有人能帮助我。我想用 Canvas (android) 画一条 Swing 或波浪线,但我不知道该怎么做。它将成为蝌蚪的尾部,所以理想情况下我希望它的形状更像三角形,一端更大
这个问题已经有答案了: Checking Collision of Shapes with JavaFX (1 个回答) 已关闭 8 年前。 我正在使用 JavaFx 8 库。 我的任务很简单:我想检
如何按编号的百分比拆分文件。行数? 假设我想将我的文件分成 3 个部分(60%/20%/20% 部分),我可以手动执行此操作,-_-: $ wc -l brown.txt 57339 brown.tx
我正在努力实现这样的目标: 但这就是我设法做到的。 你能帮我实现预期的结果吗? 更新: 如果我删除 bootstrap.css 依赖项,问题就会消失。我怎样才能让它与 Bootstrap 一起工作?
我目前正在构建一个网站,但遇到了 transform: scale 的问题。我有一个按钮,当用户将鼠标悬停在它上面时,会发生两件事: 背景以对 Angular 线“扫过” 按钮标签颜色改变 按钮稍微变
我需要使用直线和仿射变换绘制大量数据点的图形(缩放图形以适合 View )。 目前,我正在使用 NSBezierPath,但我认为它效率很低(因为点在绘制之前被复制到贝塞尔路径)。通过将我的数据切割成
我正在使用基于 SVM 分类的 HOG 特征检测器。我可以成功提取车牌,但提取的车牌除了车牌号外还有一些不必要的像素/线。我的图像处理流程如下: 在灰度图像上应用 HOG 检测器 裁剪检测到的区域 调
我有以下图片: 我想填充它的轮廓(即我想在这张图片中填充线条)。 我尝试了形态学闭合,但使用大小为 3x3 的矩形内核和 10 迭代并没有填满整个边界。我还尝试了一个 21x21 内核和 1 迭代,但
我必须找到一种算法,可以找到两组数组之间的交集总数,而其中一个数组已排序。 举个例子,我们有这两个数组,我们向相应的数字画直线。 这两个数组为我们提供了总共 7 个交集。 有什么样的算法可以帮助我解决
简单地说 - 我想使用透视投影从近裁剪平面绘制一条射线/线到远裁剪平面。我有我认为是使用各种 OpenGL/图形编程指南中描述的方法通过单击鼠标生成的正确标准化的世界坐标。 我遇到的问题是我的光线似乎
我是一名优秀的程序员,十分优秀!