解密诡异并发问题的幕后黑手:可见性问题
摘要:可见性问题还是由CPU的缓存导致的,而缓存导致的可见性问题是导致诸多诡异的并发编程问题的“幕后黑手”之一。
本文分享自华为云社区《【高并发】一文解密诡异并发问题的第一个幕后黑手——可见性问题》,作者:冰 河。
并发编程一直是很让人头疼的问题,因为多线程环境下不太好定位问题,它不像一般的业务代码那样打个断点,debug一下基本就能够定位问题所在。并发编程中,出现的问题往往都是很诡异的,而且大多数情况下,问题也不是每次都会重现的。那么,我们如何才能够更好的解决并发问题呢?这就需要我们了解造成这些问题的“幕后黑手”究竟是什么!
可见性
对于什么是可见性,比较官方的解释就是:一个线程对共享变量的修改,另一个线程能够立刻看到。
说的直白些,就是两个线程共享一个变量,无论哪一个线程修改了这个变量,则另外的一个线程都能够看到上一个线程对这个变量的修改。这里的共享变量,指的是多个线程都能够访问和修改这个变量的值,那么,这个变量就是共享变量。
例如,线程A和线程B,它们都是直接修改主内存中的共享变量,无论是线程A修改了共享变量,还是线程B修改了共享变量,则另一个线程从主内存中读取出来的变量值,一定是修改过的值,这就是线程的可见性。
可见性问题
可见性问题,可以这样理解:一个线程修改了共享变量,另一个线程不能立刻看到,这是由CPU添加了缓存导致的问题。
理解了什么是可见性,再来看可见性问题就比较好理解了。既然可见性是一个线程修改了共享变量后,另一个线程能够立刻看到对共享变量的修改,如果不能立刻看到,这就会产生可见性的问题。
单核CPU不存在可见性问题
理解可见性问题我们还需要注意一点,那就是 在单核CPU上不存在可见性问题。 这是为什么呢?
因为在单核CPU上,无论创建了多少个线程,同一时刻只会有一个线程能够获取到CPU的资源来执行任务,即使这个单核的CPU已经添加了缓存。这些线程都是运行在同一个CPU上,操作的是同一个CPU的缓存,只要其中一个线程修改了共享变量的值,那另外的线程就一定能够访问到修改后的变量值。
多核CPU存在可见性问题
单核CPU由于同一时刻只会有一个线程执行,而每个线程执行的时候操作的都是同一个CPU的缓存,所以,单核CPU不存在可见性问题。但是到了多核CPU上,就会出现可见性问题了。
这是因为在多核CPU上,每个CPU的内核都有自己的缓存。当多个不同的线程运行在不同的CPU内核上时,这些线程操作的是不同的CPU缓存。一个线程对其绑定的CPU的缓存的写操作,对于另外一个线程来说,不一定是可见的,这就造成了线程的可见性问题。
例如,上面的图中,由于CPU是多核的,线程A操作的是CPU-01上的缓存,线程B操作的是CPU-02上的缓存,此时,线程A对变量V的修改对线程B是不可见的,反之亦然。
Java中的可见性问题
使用Java语言编写并发程序时,如果线程使用变量时,会把主内存中的数据复制到线程的私有内存,也就是工作内存中,每个线程读写数据时,都是操作自己的工作内存中的数据。
此时,Java中线程读写共享变量的模型与多核CPU类似,原因是Java并发程序运行在多核CPU上时,线程的私有内存,也就是工作内存就相当于多核CPU中每个CPU内核的缓存了。
由上图,同样可以看出,线程A对共享变量的修改,线程B不一定能够立刻看到,这也就会造成可见性的问题。
代码示例
我们使用一个Java程序来验证多线程的可见性问题,在这个程序中,定义了一个long类型的成员变量count,有一个名称为addCount的方法,这个方法中对count的值进行加1操作。同时,在execute方法中,分别启动两个线程,每个线程调用addCount方法1000次,等待两个线程执行完毕后,返回count的值,代码如下所示。
package io.mykit.concurrent.lab01; /** * @author binghe * @version 1.0.0 * @description 测试可见性 */ public class ThreadTest { private long count = 0; private void addCount(){ count ++; } public long execute() throws InterruptedException { Thread threadA = new Thread(() -> { for(int i = 0; i < 1000; i++){ addCount(); } }); Thread threadB = new Thread(() -> { for(int i = 0; i < 1000; i++){ addCount(); } }); //启动线程 threadA.start(); threadB.start(); //等待线程执行完成 threadA.join(); threadB.join(); return count; } public static void main(String[] args) throws InterruptedException { ThreadTest threadTest = new ThreadTest(); long count = threadTest.execute(); System.out.println(count); } }
我们运行下这个程序,结果如下图所示。
可以看到这个程序的结果是1509,而不是我们期望的2000。这是为什么呢?让我们一起来分析下这个程序。
首先,变量count属于ThreadTest类的成员变量,这个成员变量对于线程A和线程B来说,是一个共享变量。假设线程A和线程B同时执行,它们同时将count=0读取到各自的工作内存中,每个线程第一次执行完count++操作后,同时将count的值写入内存,此时,内存中count的值为1,而不是我们想象的2。而在整个计算的过程中,线程A和线程B都是基于各自工作内存中的count值进行计算。这就导致了最终的count值小于2000。
归根结底:可见性的问题是CPU的缓存导致的。
总结
可见性是一个线程对共享变量的修改,另一个线程能够立刻看到,如果不能立刻看到,就可能会产生可见性问题。在单核CPU上是不存在可见性问题的,可见性问题主要存在于运行在多核CPU上的并发程序。归根结底,可见性问题还是由CPU的缓存导致的,而缓存导致的可见性问题是导致诸多诡异的并发编程问题的“幕后黑手”之一。我们只有深入理解了缓存导致的可见性问题,并在实际工作中时刻注意避免可见性问题,才能更好的编写出高并发程序。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
开发者测试你必须知道的7件事
摘要:开发者测试是现代软件工程中非常重要的一环,敏捷开发、主干开发这些先进的项目管理方法和流程都基于完善的开发者测试。 一、“开发者测试” 就是“开发者来测试” 开发者测试是现代软件工程中非常重要的一环,敏捷开发、主干开发这些先进的项目管理方法和流程都基于完善的开发者测试。当每个月甚至每周都要交付一个版本时,不可能投入大量的测试工程师来进行大规模的系统级别测试,这时候就需要把整个测试金字塔中的绝大部分测试通过自动化来完成。 我们今天谈开发者测试,什么是“开发者测试”? 我司有清晰的开发与测试之分。写代码归开发攻城狮,测试归测试攻城狮,大部分情况下双方处于“红蓝对峙”状态。这与我10多年前的研发团队状况非常相似。而现在的软件工程,专职的“测试攻城狮”非常少,很多公司开发测试比例大于10:1,甚至一些部门没有测试攻城狮一说。 而测试攻城狮的角色不再是手动跑测试用例的“苦力”,而是管理产品的测试系统,对产品测试进行规划、分析;归纳功能测试的思维导图、设计测试用例及带领研发团队进行测试工作,更像一位“测试专家/测试教练”。 举个简单的例子,我之前做的产品是在线视频会议协作产品,我们每天的线上例...
- 下一篇
性能提升1400+倍,快来看MySQL Volcano模型迭代器的谓词位置优化详解
摘要:性能提升1400+倍,快来看MySQL Volcano模型迭代器的谓词位置优化详解。 本文分享自华为云社区《华为云数据库内核专家为您揭秘MySQL Volcano模型迭代器性能提升千倍的秘密》,作者:GaussDB 数据库 。 20年以上数据库内核研发经验。原IBM DB2数据库内核专家,专长数据库内核性能优化、SQL查询优化、MPP分布式数据仓库技术等。现就职于华为加拿大研究所,全程参与了RDS for MySQL以及GaussDB(for MySQL)的研发工作,熟悉GaussDB(for MySQL) 全栈技术。负责NDP的总体架构设计和实现,并成功落地上线。拥有多项技术发明专利,并co-author了SIGMOD 2020 Taurus( GaussDB(for MySQL)) Paper,目前专注于下一代云数据库智能优化器的研究。 一、背景介绍 MySQL 8.0.18引入了一个新的SQL执行引擎,它遵循了Volcano模型。该模型的关键思想是将所有操作建模为“迭代器”。迭代器提供基本迭代组件:初始化、迭代和终止。所有迭代器都提供如以上相同的接口,因此迭代器可以任意组合...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合Redis,开启缓存,提高访问速度