冰河技术
导读
♻学习路线
  • 面试必问系列

    • 面试必问
  • 架构与模式

    • Java极简设计模式
    • 实战高并发设计模式
  • Java核心技术

    • Java8新特性
    • IOC核心技术
    • JVM调优技术
  • 容器化核心技术

    • Dockek核心技术
  • 分布式存储

    • Mycat核心技术
  • 数据库核心技术

    • MySQL基础篇
  • 服务器核心技术

    • Nginx核心技术
  • 渗透核心技术

    • 渗透实战技术
  • 底层技术
  • 源码分析
  • 基础案例
  • 实战案例
  • 面试
  • 系统架构
  • Spring6核心技术
  • 分布式事务

    • 分布式事务系列视频
  • SpringBoot
  • SpringCloudAlibaba
  • 🔥AI大模型项目

    • 一站式AI智能平台
    • AI智能客服系统
    • AI智能问答系统
    • 实战AI大模型
  • 中间件项目

    • 手写高性能Redis组件
    • 手写高性能脱敏组件
    • 手写线程池项目
    • 手写高性能SQL引擎
    • 手写高性能Polaris网关
    • 手写高性能RPC项目
  • 高并发项目

    • 分布式IM即时通讯系统(新)
    • 分布式Seckill秒杀系统
    • 实战高并发设计模式
  • 微服务项目

    • 简易电商脚手架项目
  • 手撕源码

    • 手撕Spring6源码
🌍知识星球
  • 总览

    • 《书籍汇总》
  • 出版图书

    • 《深入理解高并发编程:核心原理与案例实战》
    • 《深入理解高并发编程:JDK核心技术》
    • 《深入高平行開發:深度原理&專案實戰》
    • 《深入理解分布式事务:原理与实战》
    • 《MySQL技术大全:开发、优化与运维实战》
    • 《海量数据处理与大数据技术实战》
  • 电子书籍

    • 《实战高并发设计模式》
    • 《深入理解高并发编程(第2版)》
    • 《深入理解高并发编程(第1版)》
    • 《从零开始手写RPC框架(基础篇)》
    • 《SpringCloud Alibaba实战》
    • 《冰河的渗透实战笔记》
    • 《MySQL核心知识手册》
    • 《Spring IOC核心技术》
  • 关于自己
  • 关于学习
  • 关于职场
B站
Github
导读
♻学习路线
  • 面试必问系列

    • 面试必问
  • 架构与模式

    • Java极简设计模式
    • 实战高并发设计模式
  • Java核心技术

    • Java8新特性
    • IOC核心技术
    • JVM调优技术
  • 容器化核心技术

    • Dockek核心技术
  • 分布式存储

    • Mycat核心技术
  • 数据库核心技术

    • MySQL基础篇
  • 服务器核心技术

    • Nginx核心技术
  • 渗透核心技术

    • 渗透实战技术
  • 底层技术
  • 源码分析
  • 基础案例
  • 实战案例
  • 面试
  • 系统架构
  • Spring6核心技术
  • 分布式事务

    • 分布式事务系列视频
  • SpringBoot
  • SpringCloudAlibaba
  • 🔥AI大模型项目

    • 一站式AI智能平台
    • AI智能客服系统
    • AI智能问答系统
    • 实战AI大模型
  • 中间件项目

    • 手写高性能Redis组件
    • 手写高性能脱敏组件
    • 手写线程池项目
    • 手写高性能SQL引擎
    • 手写高性能Polaris网关
    • 手写高性能RPC项目
  • 高并发项目

    • 分布式IM即时通讯系统(新)
    • 分布式Seckill秒杀系统
    • 实战高并发设计模式
  • 微服务项目

    • 简易电商脚手架项目
  • 手撕源码

    • 手撕Spring6源码
🌍知识星球
  • 总览

    • 《书籍汇总》
  • 出版图书

    • 《深入理解高并发编程:核心原理与案例实战》
    • 《深入理解高并发编程:JDK核心技术》
    • 《深入高平行開發:深度原理&專案實戰》
    • 《深入理解分布式事务:原理与实战》
    • 《MySQL技术大全:开发、优化与运维实战》
    • 《海量数据处理与大数据技术实战》
  • 电子书籍

    • 《实战高并发设计模式》
    • 《深入理解高并发编程(第2版)》
    • 《深入理解高并发编程(第1版)》
    • 《从零开始手写RPC框架(基础篇)》
    • 《SpringCloud Alibaba实战》
    • 《冰河的渗透实战笔记》
    • 《MySQL核心知识手册》
    • 《Spring IOC核心技术》
  • 关于自己
  • 关于学习
  • 关于职场
B站
Github
  • 底层技术

    • 关于我
  • 源码分析

    • 一文搞懂线程与多线程
    • 如何确保线程按照我们想要的顺序执行
    • 深入解析Callable接口
    • 两种异步模型与深度解析Future接口
    • SimpleDateFormat类到底为啥不是线程安全的?
    • 不得不说的线程池与ThreadPoolExecutor类浅析
    • 深度解析线程池中那些重要的顶层接口和抽象类
    • 从源码角度分析创建线程池究竟有哪些方式
    • 通过源码深度解析ThreadPoolExecutor类是如何保证线程池正确运行的
    • 通过ThreadPoolExecutor类的源码深度解析线程池执行任务的核心流程
    • 通过源码深度分析线程池中Worker线程的执行流程
    • 从源码角度深度解析线程池是如何实现优雅退出的
    • ScheduledThreadPoolExecutor与Timer的区别和简单示例
    • 深度解析ScheduledThreadPoolExecutor类的源代码
    • 浅谈AQS中的CountDownLatch、Semaphore与CyclicBarrier
    • 浅谈AQS中的ReentrantLock、ReentrantReadWriteLock、StampedLock与Condition
    • 朋友去面试竟然栽在了Thread类的源码上
    • 如何使用Java7提供的ForkJoin框架实现高并发程序?
  • 基础案例

    • 明明中断了线程,却为何不起作用呢?
    • 由InterruptedException异常引发的思考
    • 要想学好并发编程,关键是要理解这三个核心问题
    • 要想学好并发编程,关键是要理解这三个核心问题
    • 一文解密诡异并发问题的第一个幕后黑手:可见性问题
    • 解密导致并发问题的第二个幕后黑手:原子性问题
    • 解密导致并发问题的第三个幕后黑手:有序性问题
    • 一文秒懂Happens-Before原则
  • 实战案例

    • 关于我
  • 面试

    • 关于我
  • 系统架构

    • 关于我

【高并发】解密导致并发问题的第三个幕后黑手:有序性问题

大家好,我是冰河~~

今天,我们继续聊【高并发】相关的话题,今天我们一起聊聊导致并发问题的第三个幕后黑手——有序性问题。

写在前面

大冰:小菜童鞋,昨天的内容复习了吗?

小菜:复习了大冰哥,昨天的内容干货满满啊,感觉自己收获很大。

大冰:那你说说昨天都讲了哪些内容呢?

小菜:昨天主要讲了原子性、线程切换和原子性问题,在编程语言中的一条语句可能会对应CPU中的多条指令,而CPU只能保证指令级别的原子性,不能保证编程语言级别的原子性,我们在编写并发程序时,需要自行确保编程语言级别语句的原子性。

大冰:很好,小菜童鞋,理解的不错,今天我们就来学习下引起并发编程各种诡异Bug的最后一个“幕后黑手”,也是最后一个引起并发编程Bug的源头。

有序性

有序性是指:按照代码的既定顺序执行。

说的通俗一点,就是代码会按照指定的顺序执行,例如,按照程序编写的顺序执行,先执行第一行代码,再执行第二行代码,然后是第三行代码,以此类推。如下图所示。

指令重排序

编译器或者解释器为了优化程序的执行性能,有时会改变程序的执行顺序。但是,编译器或者解释器对程序的执行顺序进行修改,可能会导致意想不到的问题!

在单线程下,指令重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。

如果发生了指令重排序,则程序可能先执行第一行代码,再执行第三行代码,然后执行第二行代码,如下所示。

例如下面的三行代码。

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锁。

我们可以将上述过程简化成下图所示。

总结

导致并发编程产生各种诡异问题的根源有三个:缓存导致的可见性问题、线程切换导致的原子性问题和编译优化带来的有序性问题。我们从根源上理解了这三个问题产生的原因,能够帮助我们更好的编写高并发程序。

如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。

写在最后

大冰:好了,今天就是我们讲的主要内容了,今天的内容同样最重要,回去后要好好复习。

小菜:好的,大冰哥,一定好好复习。

最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。

写在最后

如果你觉得冰河写的还不错,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发、分布式、微服务、大数据、互联网和云原生技术,「 冰河技术 」微信公众号更新了大量技术专题,每一篇技术文章干货满满!不少读者已经通过阅读「 冰河技术 」微信公众号文章,吊打面试官,成功跳槽到大厂;也有不少读者实现了技术上的飞跃,成为公司的技术骨干!如果你也想像他们一样提升自己的能力,实现技术能力的飞跃,进大厂,升职加薪,那就关注「 冰河技术 」微信公众号吧,每天更新超硬核技术干货,让你对如何提升技术能力不再迷茫!

在 GitHub 上编辑此页
上次更新: 2026/4/29 16:18
Contributors: binghe001
Prev
解密导致并发问题的第二个幕后黑手:原子性问题
Next
一文秒懂Happens-Before原则
阅读全文
×

扫码或搜索:冰河技术
发送:290992
即可立即永久解锁本站全部文章

星球会员
跳转链接