随着多核架构的普及,并发编程已成为开发者不可或缺的核心技能。在学习过程中,开发者常会遇到这样的困惑:正确编写的单线程代码,为何在并发环境下可能瞬间失效?看似有序的语句执行后,为何结果却混乱不堪?这些问题,都指向了编程领域的一个关键课题——内存模型。

本文以Java语言为例,剖析共享数据在并发环境中的传播机制、指令执行的有序性保障,以及原子操作的实现原理,从而揭示多线程程序从代码到处理器执行的底层逻辑。同时,通过剖析工程实践中常见的并发异常,并追溯其根本原因,帮助读者构建对并发编程本质的系统理解。

并发之谜:为何我的代码背叛了我?

在并发编程中,共享变量是指能够被多个线程同时访问的变量,如全局变量、静态变量或对象的实例成员变量。这些变量通常存储在堆内存中,而非线程私有的栈内存中,因为堆内存对所有线程可见。

共享变量为线程间通信提供了便利,允许线程通过读写这些变量来交换信息和协调任务。然而,这种共享机制也带来了复杂性。当多线程同时读写共享变量且缺乏保护措施时,可能引发数据不一致、程序异常甚至系统崩溃等后果。

private int a, b;
private int x, y; public void test() {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
}); Thread t2 = new Thread(() -> {
b = 2;
y = a;
}); // ...start启动线程,join等待线程
assert x == 2;
assert y == 1;
}

首先,考虑如上代码片段:定义了两个共享变量 x 和 y,并在两个线程中分别对它们进行赋值。当同时启动这两个线程并等待它们执行完毕后,x 是否等于 2 且 y 等于 1 呢?答案是不确定的,因为共享变量 x 和 y 可能存在多种执行结果。这种现象在并发编程中并不罕见,常常会导致程序逻辑与预期不符,进而引发困惑。

然而,通过深入分析这些问题的根源,可以发现它们并非无迹可寻。主要原因可以归结为两点:首先,处理器与内存之间对共享变量的处理速度存在差异,这会导致可见性问题。其次,编译器和处理器可能会对代码指令进行重排序优化,从而导致有序性问题。

可见性:你看到的是真相吗?



如上图所示,由于处理器和内存之间的速度差异显著,为了提高处理效率,处理器并不直接与内存进行通信,而是先将系统内存中的数据加载到处理器内部的缓存(如L1、L2或其他级别缓存)中,然后再进行操作。这一机制基于局部性原理,即处理器在读取内存数据时,通常以块为单位进行读取,每一块数据称为缓存行(Cache Line)。当处理器完成对数据的操作后,并不会立即将结果写回内存,而是先写入缓存中,并将该缓存行标记为脏(Dirty)状态。只有当该缓存行被替换时,数据才会被写回内存。这一过程被称为写回策略(Write Back)。

此外,处理器还引入了写缓冲区(Store Buffer)来进一步提升效率。写缓冲区用于临时保存处理器向内存写入的数据,使得处理器在写入数据时无需等待慢速的内存操作完成,从而可以继续执行后续指令,确保指令流水线的持续运行。然而,这种优化机制也带来了潜在的问题:由于写缓冲区中的数据并不会立即写回内存,且写缓冲区仅对当前处理器可见,其他处理器无法即时感知共享变量的变更。这可能导致处理器的读写顺序与内存实际操作的读写顺序不一致,从而引发可见性和有序性问题,进一步增加了并发编程的复杂性。

现在再回来看上面代码,那么可以得到四种结果:

1)假设处理器A对变量a赋值,但没及时回写内存。处理器B对变量b赋值,且及时回写内存。处理器A从内存中读到变量b最新值。那么这时结果是:x等于2,y等于0;

2)假设处理器A对变量a赋值,且及时回写内存。处理器B从内存中读到变量a最新值。处理器B对变量b赋值,但没及时回写内存。那么这时结果是:x等于0,y等于1;

3)假设处理器A和B,都没及时回写变量a和b值到内存。那么这时结果是:x等于0,y等于0;

4)假设处理器A和B,都及时回写变量a和b值到内存,且从内存中读到变量a和b的最新值。那么这时结果是:x等于2,y等于1。

从上面可发现:除了第四种情况,其他三种情况都存在对共享变量的操作不可见。所谓可见性,便是当一个线程对某个共享变量的操作,另外一个线程立即可见这个共享变量的变更。

而从上面推论可以发现,要达到可见性,需要处理器及时回写共享变量最新值到内存,也需要其他处理器及时从内存中读取到共享变量最新值。

因此也可以说只要满足上述两个条件。那么就可以保证对共享变量的操作,在并发情况下是线程安全的。在Java语言中,是通过volatile关键字实现。volatile 关键字并不是 Java 语言的特产,古老的 C 语言里也有,它最原始的意义就是禁用处理器缓存。

对如下代码中的共享变量:

// instance是volatile变量
volatile Singlenton instance = new Singlenton();

转换成汇编代码,如下:

0x01a3de1d: movb 5 0 x 0, 0 x 1104800(% esi);
0x01a3de24: lock addl $ 0 x 0,(% esp);

可以看到volatile修饰的共享变量会多出第二行汇编变量,并且多了一个LOCK指令。LOCK前缀的指令在多核处理器会引发两件事:

1)将当前处理器缓存行的数据写回到系统内存;

2)这个写回内存的操作会使在其他处理器里缓存了该内存地址的数据无效。

上述的操作是通过总线嗅探和总线仲裁来实现。而基于总线嗅探和总线仲裁,现代处理器逐渐形成了各种缓存一致性协议,例如 MESI 协议。

总之操作系统便是基于上述实现,从底层来保证共享变量在并发情况下的线程安全。而对实际开发,只需要在恰当时候加上volatile关键字就可以。

除了volatile,也可以使用synchronized关键字来保证可见性。 不同于volatile,synchronized通过两个操作来保证内存可见性:获取锁和释放锁。当一个线程获取锁时,它会清空工作内存中的共享变量,并从主内存中重新加载最新的值。这样,其他线程在获取锁之前无法访问该变量,从而保证了内存可见性。当线程释放锁时,它会将工作内存中的值刷新回主内存,以便其他线程可以看到最新的值。

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!!!

我的代码背叛了我?为什么 a=1, b=2,最后x和y都等于0?的更多相关文章

  1. java子父类初始化顺序 (1)父类静态代码块(2)父类静态变量初始化(3)子类静态代码块(4)子类静态变量初始化(5)main(6)有对象开辟空间都为0(7)父类显示初始化(8)父类构造(9)子类显示初始化(10)子类构造

    标题 静态代码块与静态成员变量还要看代码的先后顺序 看程序,说出结果 结果为: x=0 看程序,说出结果 结果如下: 补充 : 静态代码块:static{ } 在JVM加载时即执行,先于主方法执行,用 ...

  2. 编译OpenGL代码时发生 Inconsistency detected by ld.so: dl-version.c: 224: _dl_check_map_versions: Assertion `needed != ((void *)0)' failed! 错误的解决方案

    注:本解决方案适用于使用N卡的PC 出现该错误 , 一般是由于开源的nouveau驱动和Nvidia专有驱动冲突导致的 .在解决该问题时 , 尝试过卸载 N 卡专有驱动 , 仅使用开源nouveau驱 ...

  3. spark最新源码下载并导入到开发环境下助推高质量代码(Scala IDEA for Eclipse和IntelliJ IDEA皆适用)(以spark2.2.0源码包为例)(图文详解)

    不多说,直接上干货! 前言   其实啊,无论你是初学者还是具备了有一定spark编程经验,都需要对spark源码足够重视起来. 本人,肺腑之己见,想要成为大数据的大牛和顶尖专家,多结合源码和操练编程. ...

  4. C#代码实现 Excel表格与Object互相转换,Excel表格导入数据库(.NET2.0 .NET4.0)

    前些天在工作上遇到这个需求,在GitHub找到一个开源代码可以用,Fork了一个版本,整理一下发出来. ①.Net项目中使用Nuget安装一个 NPOI 包    https://github.com ...

  5. Win7旗舰版中的IIS配置asp.net 完美通过版,附代码 以及出现的 CS0016: 未能写入输出文件“c:\Windows\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files\root\8d57d

    先解决问题:“c:\Windows\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files\root\8d57d 图: 其他的解决方案 ...

  6. 代码实现:企业发放的奖金根据利润提成。利润(I)低于或等于10万元时,奖金可提10%; 利润高于10万元,低于20万元时,低于10万元的部分按10%提成,高于10万元的部分,可可提成7.5%; 20万到40万之间时,高于20万元的部分,可提成5%;40万到60万之间时高于40万元的部分,可提成3%; 60万到100万之间时,高于60万元的部分,可提成1.5%,高于100万元时,超过100万元

    import java.util.Scanner; /* 企业发放的奖金根据利润提成.利润(I)低于或等于10万元时,奖金可提10%: 利润高于10万元,低于20万元时,低于10万元的部分按10%提成 ...

  7. Machine Learning - 第3周(Logistic Regression、Regularization)

    Logistic regression is a method for classifying data into discrete outcomes. For example, we might u ...

  8. 吴恩达-coursera-机器学习-week3

    六.逻辑回归(Logistic Regression) 6.1 分类问题 6.2 假说表示 6.3 判定边界 6.4 代价函数 6.5 简化的成本函数和梯度下降 6.6 高级优化 6.7 多类别分类: ...

  9. LuoguP7441 「EZEC-7」Erinnerung 题解

    Content 给定 \(x,y,K\).定义两个数列 \(c,e\),其中 \(c_i=\begin{cases}x\cdot i&x\cdot i\leqslant K\\-K&\ ...

  10. 一元回归1_基础(python代码实现)

    python机器学习-乳腺癌细胞挖掘(博主亲自录制视频) https://study.163.com/course/introduction.htm?courseId=1005269003&u ...

随机推荐

  1. Axure通用电商后台管理系高保真交互模板原型图附元件库4种后台模板风格

    Axure通用电商后台管理交互模板原型图附元件库4种后台模板风格,原型中使用4种不同的布局框架,你可以根据自己的需求,去选中对应的菜单排版布局.另外,原型图中使用了较多的交互元件.母版.动态面板,基本 ...

  2. C#开发的Panel滚动分页控件 - 开源研究系列文章

    前些时候因为想拥有一个自己的软件快捷打开软件,于是参考Windows 11的开始菜单,进行了编写这个应用软件,里面有一个功能就是对显示的Panel里的应用对象的分页功能,于是就想写一个对Panel的自 ...

  3. 开源我的一款自用AI阅读器,引流Web前端、Rust、Tauri、AI应用开发

    前沿 - 为什么要做这个开源软件 作为一个典型的前端开发者,去年在为公司调研Rust前端工具链.LLM应用开发技术体系的时候,对这类技术领域产生了浓厚的兴趣,也是出于早期曾经是一名Android移动应 ...

  4. JavaScript中通过闭包来实现私有变量的一种方法

    'use strict'; const SecretHolder = (function () { const secrets = new WeakMap(); return class { cons ...

  5. B1021 个位数统计

    描述 输入格式: 每个输入包含 1 个测试用例,即一个不超过 1000 位的正整数 N. 输出格式: 对 N 中每一种不同的个位数字,以 D:M 的格式在一行中输出该位数字 D 及其在 N 中出现的次 ...

  6. SgLang代码细读-2.forward过程

    SgLang代码细读-2.forward过程 总览 Forward的主要过程围绕着 run_batch->TPModelWorker->ModelRunner->Model-> ...

  7. 使用 Linux 命令 curl 和 telnet 测试接口连通性

    摘要:接口可用性诊断利器curl和Telnet. 综述   Linux 中的命令 curl 是利用 URL 语法在命令行模式下工作的开源文件传输工具,它可以被用于测试API接口,查看响应头和发出HTT ...

  8. Java 提取url的域名

      有时候,我们需要校验URL的域名是否在白名单中,故需要提取其中的域名.可以使用java标准类库java.net.URL进行提取,方法如下: import org.apache.commons.la ...

  9. 「Temp」目录

    吃吃吃 \(\color{orange}{Eon\ 今天吃什么}\) Temp \(\color{magenta}{代码模板}\) Trick \(\color{magenta}{常见错误}\) \( ...

  10. 【中文】【吴恩达课后编程作业】Course 2 - 改善深层神经网络 - 第三周作业

    [中文][吴恩达课后编程作业]Course 2 - 改善深层神经网络 - 第三周作业 - TensorFlow入门 上一篇:[课程2 - 第三周测验]※※※※※ [回到目录]※※※※※下一篇:[课程3 ...