- 使用 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缓存一致性原理图解如下:
多线程同步问题如下:
关闭。这个问题是off-topic .它目前不接受答案。 想要改进这个问题? Update the question所以它是on-topic用于堆栈溢出。 关闭 12 年前。 Improve thi
我有一个动态网格,其中的数据功能需要正常工作,这样我才能逐步复制网格中的数据。假设在第 5 行中,我输入 10,则从第 6 行开始的后续行应从 11 开始读取,依此类推。 如果我转到空白的第一行并输入
我有一个关于我的按钮消失的问题 我已经把一个图像作为我的按钮 用这个函数动画 function example_animate(px) { $('#cont
我有一个具有 Facebook 连接和经典用户名/密码登录的网站。目前,如果用户单击 facebook_connect 按钮,系统即可运行。但是,我想将现有帐户链接到 facebook,因为用户可以选
我有一个正在为 iOS 开发的应用程序,该应用程序执行以下操作 加载和设置注释并启动核心定位和缩放到位置。 map 上有很多注释,从数据加载不会花很长时间,但将它们实际渲染到 map 上需要一段时间。
我被推荐使用 Heroku for Ruby on Rails 托管,到目前为止,我认为我真的会喜欢它。只是想知道是否有人可以帮助我找出问题所在。 我按照那里的说明在该网站上创建应用程序,创建并提交
我看过很多关于 SSL 错误的帖子和信息,我自己也偶然发现了一个。 我正在尝试使用 GlobalSign CA BE 证书通过 Android WebView 访问网页,但出现了不可信错误。 对于大多
我想开始使用 OpenGL 3+ 和 4,但我在使用 Glew 时遇到了问题。我试图将 glew32.lib 包含在附加依赖项中,并且我已将库和 .dll 移动到主文件夹中,因此不应该有任何路径问题。
我已经盯着这两个下载页面的源代码看了一段时间,但我似乎找不到问题。 我有两个下载页面,一个 javascript 可以工作,一个没有。 工作:http://justupload.it/v/lfd7不是
我一直在使用 jQuery,只是尝试在单击链接时替换文本字段以及隐藏/显示内容项。它似乎在 IE 中工作得很好,但我似乎无法让它在 FF 中工作。 我的 jQuery: $(function() {
我正在尝试为 NDK 编译套接字库,但出现以下两个错误: error: 'close' was not declared in this scope 和 error: 'min' is not a m
我正在使用 Selenium 浏览器自动化框架测试网站。在测试过程中,我切换到特定的框架,我们将其称为“frame_1”。后来,我在 Select 类中使用了 deselectAll() 方法。不久之
我正在尝试通过 Python 创建到 Heroku PostgreSQL 数据库的连接。我将 Windows10 与 Python 3.6.8 和 PostgreSQL 9.6 一起使用。 我从“ht
我有一个包含 2 列的数据框,我想根据两列之间的比较创建第三列。 所以逻辑是:第 1 列 val = 3,第 2 列 val = 4,因此新列值什么都没有 第 1 列 val = 3,第 2 列 va
我想知道如何调试 iphone 5 中的 css 问题。 我尝试使用 firelite 插件。但是从纵向旋转到横向时,火石占据了整个屏幕。 有没有其他方法可以调试 iphone 5 中的 css 问题
所以我有点难以理解为什么这不起作用。我正在尝试替换我正在处理的示例站点上的类别复选框。我试图让它做以下事情:未选中时以一种方式出现,悬停时以另一种方式出现(选中或未选中)选中时以第三种方式出现(而不是
Javascript CSS 问题: 我正在使用一个文本框来写入一个 div。我使用以下 javascript 获取文本框来执行此操作: function process_input(){
你好,我很难理解 P、NP 和多项式时间缩减的主题。我试过在网上搜索它并问过我的一些 friend ,但我没有得到任何好的答案。 我想问一个关于这个话题的一般性问题: 设 A,B 为 P 中的语言(或
你好,我一直在研究 https://leetcode.com/problems/2-keys-keyboard/并想到了这个动态规划问题。 您从空白页上的“A”开始,完成后得到一个数字 n,页面上应该
我正在使用 Cocoapods 和 KIF 在 Xcode 服务器上运行持续集成。我已经成功地为一个项目设置了它来报告每次提交。我现在正在使用第二个项目并收到错误: Bot Issue: warnin
我是一名优秀的程序员,十分优秀!