每日一博 | 并发高?可能是编译优化引发有序性问题
摘要:CPU为了对程序进行优化,会对程序的指令进行重排序,此时程序的执行顺序和代码的编写顺序不一定一致,这就可能会引起有序性问题。
本文分享自华为云社区《【高并发】解密导致并发问题的第三个幕后黑手——有序性问题》,作者:冰 河 。
有序性
有序性是指:按照代码的既定顺序执行。
说的通俗一点,就是代码会按照指定的顺序执行,例如,按照程序编写的顺序执行,先执行第一行代码,再执行第二行代码,然后是第三行代码,以此类推。如下图所示。
指令重排序
编译器或者解释器为了优化程序的执行性能,有时会改变程序的执行顺序。但是,编译器或者解释器对程序的执行顺序进行修改,可能会导致意想不到的问题!
在单线程下,指令重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
如果发生了指令重排序,则程序可能先执行第一行代码,再执行第三行代码,然后执行第二行代码,如下所示。
例如下面的三行代码。
int x = 1; int y = 2; int z = x + y;
CPU发生指令重排序时,能够保证x=1和y = 2这两行代码在z = x + y这行代码的上面,而x = 1和 y = 2的顺序就不一定了。在单线程下不会出现问题,但是在多线程下就不一定了。
有序性问题
CPU为了对程序进行优化,会对程序的指令进行重排序,此时程序的执行顺序和代码的编写顺序不一定一致,这就可能会引起有序性问题。
在Java程序中,一个经典的案例就是使用双重检查机制来创建单例对象。例如,在下面的代码中,在getInstance()方法中获取对象实例时,首先判断instance对象是否为空,如果为空,则锁定当前类的class对象,并再次检查instance是否为空,如果instance对象仍然为空,则为instance对象创建一个实例。
package io.binghe.concurrent.lab01; /** * @author binghe * @version 1.0.0 * @description 测试单例 */ public class SingleInstance { private static SingleInstance instance; public static SingleInstance getInstance(){ if(instance == null){ synchronized (SingleInstance.class){ if(instance == null){ instance = new SingleInstance(); } } } return instance; } }
如果编译器或者解释器不会对上面的程序进行优化,整个代码的执行过程如下所示。
注意:为了让大家更加明确流程图的执行顺序,我在上图中标注了数字,以明确线程A和线程B执行的顺序。
假设此时有线程A和线程B两个线程同时调用getInstance()方法来获取对象实例,两个线程会同时发现instance对象为空,此时会同时对SingleInstance.class加锁,而JVM会保证只有一个线程获取到锁,这里我们假设是线程A获取到锁。则线程B由于未获取到锁而进行等待。接下来,线程A再次判断instance对象为空,从而创建instance对象的实例,最后释放锁。此时,线程B被唤醒,线程B再次尝试获取锁,获取锁成功后,线程B检查此时的instance对象已经不再为空,线程B不再创建instance对象。
上面的一切看起来很完美,但是这一切的前提是编译器或者解释器没有对程序进行优化,也就是说CPU没有对程序进行重排序。而实际上,这一切都只是我们自己觉得是这样的。
在真正高并发环境下运行上面的代码获取instance对象时,创建对象的new操作会因为编译器或者解释器对程序的优化而出现问题。也就是说,问题的根源在于如下一行代码。
instance = new SingleInstance();
对于上面的一行代码来说,会有3个CPU指令与其对应。
1.分配内存空间。
2.初始化对象。
3.将instance引用指向内存空间。
正常执行的CPU指令顺序为1—>2—>3,CPU对程序进行重排序后的执行顺序可能为1—>3—>2。此时,就会出现问题。
当CPU对程序进行重排序后的执行顺序为1—>3—>2时,我们将线程A和线程B调用getInstance()方法获取对象实例的两种步骤总结如下所示。
【第一种步骤】
(1)假设线程A和线程B同时进入第一个if条件判断。
(2)假设线程A首先获取到synchronized锁,进入synchronized代码块,此时因为instance对象为null,所以,此时执行instance = new SingleInstance()语句。
(3)在执行instance = new SingleInstance()语句时,线程A会在JVM中开辟一块空白的内存空间。
(4)线程A将instance引用指向空白的内存空间,在没有进行对象初始化的时候,发生了线程切换,线程A释放synchronized锁,CPU切换到线程B上。
(5)线程B进入synchronized代码块,读取到线程A返回的instance对象,此时这个instance不为null,但是并未进行对象的初始化操作,是一个空对象。此时,线程B如果使用instance,就可能出现问题!!!
【第二种步骤】
(1)线程A先进入if条件判断,
(2)线程A获取synchronized锁,并进行第二次if条件判断,此时的instance为null,执行instance = new SingleInstance()语句。
(3)线程A在JVM中开辟一块空白的内存空间。
(4)线程A将instance引用指向空白的内存空间,在没有进行对象初始化的时候,发生了线程切换,CPU切换到线程B上。
(5)线程B进行第一次if判断,发现instance对象不为null,但是此时的instance对象并未进行初始化操作,是一个空对象。如果线程B直接使用这个instance对象,就可能出现问题!!!
在第二种步骤中,即使发生线程切换时,线程A没有释放锁,则线程B进行第一次if判断时,发现instance已经不为null,直接返回instance,而无需尝试获取synchronized锁。
我们可以将上述过程简化成下图所示。
总结
导致并发编程产生各种诡异问题的根源有三个:缓存导致的可见性问题、线程切换导致的原子性问题和编译优化带来的有序性问题。我们从根源上理解了这三个问题产生的原因,能够帮助我们更好的编写高并发程序。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Apache Camel 3.11.4
Apache Camel 3.11.4 现已发布。Apache Camel 是一个开源的面向消息的中间件框架,它有一个基于规则的路由和调解引擎,提供了一个基于 Java 对象的企业集成模式的实现,使用应用编程接口来配置路由和调解规则。 在面向服务的架构项目中,Camel 经常与 Apache ServiceMix、Apache ActiveMQ 和 Apache CXF 一起使用。 主要更新内容 Bug 修复 camel-core - XPathBuilder 在使用 @XPath 注释时从不清除池并增加池导致内存泄漏 工作完成后,UnitOfWorkHelper 不会清除 UoW Camel-AWS2-SQS:消息属性最多可以有 10 个 rest-dsl - clientRequestValidation 失败,然后操作产生的不仅仅是 xml 或 json camel-karaf - 添加 camel-cxf 时出错 camel-debezium-mongodb-starter 不会从 application.properties 中提取值 抛出 JMX MBean Instanc...
- 下一篇
Just Perfection —— 用于 Gnome Shell 的自定义扩展
Just Perfection 是一个用于 Gnome Shell 的扩展。Just Perfection包括一个选项列表,用于切换 GNOME UI 元素的可见性、自定义面板大小、填充和更改行为。 具备的功能包括: 覆盖 Gnome Shell 主题以创建最小的桌面 隐藏顶栏 在概览屏幕中隐藏顶部栏 删除左上角的“活动”按钮 禁用 app-menu、clock、系统托盘菜单 在概览中删除所选应用程序的搜索框、工作区选择器、关闭按钮和标题 在屏幕显示上禁用扩展坞启动器 打开/关闭顶部栏项目的图标 删除面板箭头(应用程序菜单和电池图标后面的小三角形) 切换应用手势、输入以进行搜索 自定义面板位置(顶部或底部)、时钟位置、面板大小和按钮填充、动画速度等。 安装 GNOME Extensions 网站 此扩展可在GNOME 扩展网站上获得 手动安装 可以下载此 repo 并使用构建脚本手动安装它: $ ./scripts/build.sh -i
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8编译安装MySQL8.0.19
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS关闭SELinux安全模块
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池