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

简介: 并发编程是现代开发的核心技能。本文以Java为例,解析多线程环境下共享数据的传播机制、指令有序性保障及原子操作原理,深入探讨可见性与有序性问题的根源。通过分析典型并发异常案例,揭示处理器缓存、写缓冲区及内存模型对程序行为的影响,并详解volatile与synchronized关键字的底层实现机制,帮助开发者构建对并发编程本质的系统理解。

随着多核架构的普及,并发编程已成为开发者不可或缺的核心技能。在学习过程中,开发者常会遇到这样的困惑:正确编写的单线程代码,为何在并发环境下可能瞬间失效?看似有序的语句执行后,为何结果却混乱不堪?这些问题,都指向了编程领域的一个关键课题——内存模型。
本文以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 可能存在多种执行结果。这种现象在并发编程中并不罕见,常常会导致程序逻辑与预期不符,进而引发困惑。
然而,通过深入分析这些问题的根源,可以发现它们并非无迹可寻。主要原因可以归结为两点:首先,处理器与内存之间对共享变量的处理速度存在差异,这会导致可见性问题。其次,编译器和处理器可能会对代码指令进行重排序优化,从而导致有序性问题。

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

image.png

如上图所示,由于处理器和内存之间的速度差异显著,为了提高处理效率,处理器并不直接与内存进行通信,而是先将系统内存中的数据加载到处理器内部的缓存(如L1、L2或其他级别缓存)中,然后再进行操作。这一机制基于局部性原理,即处理器在读取内存数据时,通常以块为单位进行读取,每一块数据称为缓存行(Cache Line)。当处理器完成对数据的操作后,并不会立即将结果写回内存,而是先写入缓存中,并将该缓存行标记为脏(Dirty)状态。只有当该缓存行被替换时,数据才会被写回内存。这一过程被称为写回策略(Write Back)。
此外,处理器还引入了写缓冲区(Store Buffer)来进一步提升效率。写缓冲区用于临时保存处理器向内存写入的数据,使得处理器在写入数据时无需等待慢速的内存操作完成,从而可以继续执行后续指令,确保指令流水线的持续运行。然而,这种优化机制也带来了潜在的问题:由于写缓冲区中的数据并不会立即写回内存,且写缓冲区仅对当前处理器可见,其他处理器无法即时感知共享变量的变更。这可能导致处理器的读写顺序与内存实际操作的读写顺序不一致,从而引发可见性和有序性问题,进一步增加了并发编程的复杂性。
image.png

现在再回来看上面代码,那么可以得到四种结果:
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 协议。
image.png

总之操作系统便是基于上述实现,从底层来保证共享变量在并发情况下的线程安全。而对实际开发,只需要在恰当时候加上volatile关键字就可以。
除了volatile,也可以使用synchronized关键字来保证可见性。 不同于volatile,synchronized通过两个操作来保证内存可见性:获取锁和释放锁。当一个线程获取锁时,它会清空工作内存中的共享变量,并从主内存中重新加载最新的值。这样,其他线程在获取锁之前无法访问该变量,从而保证了内存可见性。当线程释放锁时,它会将工作内存中的值刷新回主内存,以便其他线程可以看到最新的值。

未完待续

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

目录
相关文章
|
网络协议
自己总结的wireshark抓包技巧
自己总结的wireshark抓包技巧
663 0
Apache JMeter 中的 Latency 和 Load Time
Apache JMeter 中的 Latency 和 Load Time
Apache JMeter 中的 Latency 和 Load Time
|
存储 算法 关系型数据库
|
9月前
|
人工智能 算法 API
国产化用于单导联和六导联的心电算法及API服务
随着智能设备普及,心电图功能逐渐应用于智能手表、体脂仪等设备。苏州唯理推出单导联及6导联心电算法API服务,由AI驱动,1分钟内快速评估心律失常、房颤、早搏等问题,已广泛用于医疗设备及三甲医院。其算法还可评估压力、疲劳、情绪状态,筛查效率远超进口设备。唯理率先实现国产医疗级心电芯片,支持快速集成与私有化部署,适用于多种智能硬件。
|
存储 前端开发 JavaScript
🚀前端轻松实现网页内容转换:一键复制、保存图片及生成 Markdown
在现代前端开发中,提升用户的交互体验至关重要。本文将详细介绍如何使用 HTML2Canvas 和 Turndown 两个强大的 JavaScript 库,实现将网页选中文本转化为图片并保存或复制到剪贴板,或将内容转换为 Markdown 格式。文章包含核心代码实现、技术细节和功能拓展方向,为开发者提供了一个轻量级的解决方案,提升用户体验。
1665 68
|
9月前
|
自然语言处理 前端开发 算法
Java编译器优化秘籍:字节码背后的IR魔法与常见技巧
编译器将源代码转换为机器码的过程中,会经历多个中间表达形式(IR)的转换与优化。前端生成高级IR(HIR),后端将其转为低级IR(LIR)并进行机器相关优化。Java编译流程包括源码到字节码、再由即时编译器转换为内部HIR(如SSA图)、优化后生成LIR,最终编译为机器码。常见优化技术包括常量折叠、值编号、死代码消除、公共子表达式消除等,旨在提升程序性能与执行效率。
345 0
|
10月前
|
Java
分析Java中的static、final以及static final关键字的不同用法。
综上所述,static, final以及 static final关键字在Java编程中有着明确和关键的角色。static使成员独立于类的实例而存在,final保证值或行为的不可变性,而 static final组合了静态成员的共享特性和常量的不可变性。这些特性在设计类的时候,对于资源共享、安全性保证和优化性能都是至关重要的。
451 0
|
人工智能 自然语言处理 关系型数据库
从数据到智能,一站式带你了解 Data+AI 精选解决方案、特惠权益
从 Data+AI 精选解决方案、特惠权益等,一站式带你了解阿里云瑶池数据库经典的AI产品服务与实践。
|
Web App开发 JSON 网络安全
Charles配置代理以及简单使用
Charles配置代理以及简单使用
1340 0
|
监控 开发者 Python
【Python】已解决:WARNING: This is a development server. Do not use it in a production deployment. Use a p
【Python】已解决:WARNING: This is a development server. Do not use it in a production deployment. Use a p
2256 0