有序性:代码执行的幻觉

前面讲到通过缓存一致性协议,来保障共享变量的可见性。那么是否还有其他情况,导致对共享变量操作不符合预期结果。可以看下面的代码:

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

假设将线程t1的代码块从a = 1; x = b;改成x = b; a = 1; 。将线程t2的代码块从b = 2; y = a;改成y = a; b = 2;。

对于线程t1和t2自己来说,代码的重排序,不会影响当前线程执行。但是在多线程并发执行下,会出现如下情况:

1)假设处理器A先将变量b = 0赋值给x,再将变量a赋值1。处理器B先将变量a = 0赋值给y,再将变量b赋值2。那么这时结果是:x等于0,y等于0。

可见代码的重排序也会影响到程序最终结果。

重排序是一种被编译器和处理器采用的优化策略,以便更有效地利用处理器资源,减少指令的执行延迟,以及提高并行指令的数量。

在编译阶段,编译器会进行静态重排序。例如,编译器可能会将计算密集型的指令移动到I/O操作之前,以便在等待I/O完成时,处理器可以执行其他的计算任务。

在运行阶段,现代处理器会进行动态重排序,也被称为指令重排序。例如,当一个指令需要等待数据从内存加载时,处理器可能会先执行其他没有数据依赖的指令,从而避免处理器空闲。

重排序需要遵守两点。

1)数据依赖性:如果两个操作之间存在数据依赖,那么编译器和处理器不能调整它们的顺序。

// 写后读
a = 1;
b = a;
// 写后写
a = 1;
a = 2;
// 读后写
a = b;
b = 1;

上面3种情况,编译器和处理器不能调整它们的顺序,否则将会造成程序语义的改变。

2)as-if-serial语义:即给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。

a = 1;
b = 2;
c = a * b;

如上对变量a的赋值和对变量b的赋值,不存在数据依赖关系。因此对变量a和b重排序不会影响变量c的结果。

但数据依赖性和as-if-serial语义只保证单个处理器中执行的指令序列和单个线程中执行的操作,并不考虑多核处理器和多线程之间的数据依赖情况。因此在多线程程序中,对存在数据依赖的操作重排序,可能会改变程序的执行结果。因此要避免程序的错误的执行,便是需要禁止这种编译和处理器优化导致的重排序。

这种解决重排序问题的机制,叫做内存屏障。内存屏障也被称为内存栅栏或内存栅障,是一种用于处理多处理器编程中的同步问题的计算机指令。它的主要作用是防止某些内存操作的重排序。以日常接触的 X86_64 架构来说,内存操作指令如读读(LoadLoad)、读写(LoadStore)以及写写(StoreStore)内存屏障是空操作(no-op),只有写读(StoreLoad)内存屏障会被替换成具体指令。

在Java语言中,内存屏障通过volatile关键字实现,禁止被它修饰的变量发生指令重排序操作:

1)不允许 volatile 字段写操作之前的内存访问被重排序至其之后。

2)不允许 volatile 字段读操作之后的内存访问被重排序至其之前。

//  变量a,b通过volatile修饰
private volatile int a, b;
private int x, y; public void test() {
Thread t1 = new Thread(() -> {
a = 1;
// 编译器插入storeload内存屏障指令
// 1)禁止代码和指令重排序
// 2)强制刷新变量a的最新值到内存
x = b;
// 1)强制从内存中读取变量b的最新值
}); Thread t2 = new Thread(() -> {
b = 2;
// 编译器插入storeload内存屏障指令
// 1)禁止代码和指令重排序
// 2)强制刷新变量b的最新值到内存
y = a;
// 1)强制从内存中读取变量a的最新值
}); // ...start启动线程,join等待线程
assert x == 2;
assert y == 1;
}

可以看到通过volatile修饰的变量通过LOCK指令和内存屏障,实现共享变量的可见性和避免代码和指令的重排序,最终保障了程序在多线程情况下的正常执行。

未完待续

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

我的代码出现幻觉?说好的a = 1; x = b,怎么成了x = b; a = 1?的更多相关文章

  1. 博客代码美化(SyntaxHighlighter)

    这篇博文主要讲解自己使用SyntaxHighlighter对代码进行美工中遇见的问题以及如何使用SyntaxHighlighter? 首先来看看SyntaxHighlighter对代码美工的效果吧! ...

  2. python3下载远程代码并执行

    第一步: 先在gist之类的网站上贴上代码,目的不是高亮,而可以raw的形式获取代码,这样可以省掉处理html的时间,我这里用的是pasteraw: 远程上的代码:http://cdn.pastera ...

  3. jquery动态删除html代码

    1.remove() remove()方法移除被选元素,包括所有的文本和子节点. 语法:$(selector).remove() 当我们想将元素自身移除时我们用 .remove(),同时也会移除元素内 ...

  4. 在代码中使用Autolayout – intrinsicContentSize和Content Hugging Priority

    我们继续来看在代码中使用Autolayout的话题.先说intrinsicContentSize,也就是控件的内置大小.比如UILabel,UIButton等控件,他们都有自己的内置大小.控件的内置大 ...

  5. 命令行将本地代码上传到github及修改github上代码

    第一步:建立git仓库 cd到你的本地项目根目录下,(这是我的细目目录) 执行git命令 git init 第二步:将项目的所有文件添加到仓库中 git add . 如果想添加某个特定的文件,只需把. ...

  6. Kafka - SQL 代码实现

    1.概述 上次给大家分享了关于 Kafka SQL 的实现思路,这次给大家分享如何实现 Kafka SQL.要实现 Kafka SQL,在上一篇<Kafka - SQL 引擎分享>中分享了 ...

  7. 利用eclipse抽取 代码片段为方法

    选取要被抽取成方法的代码片段,右键->Refactor--->Extract Method 填写方法名称     抽取后成了这个样子:

  8. 敏捷开发中高质量 Java 代码开发实践

    Java 项目开发过程中,由于开发人员的经验.代码风格各不相同,以及缺乏统一的标准和管理流程,往往导致整个项目的代码质量较差,难于维护,需要较大的测试投入 和周期等问题. 这些问题在一个项目组初建.需 ...

  9. QQ,MSN,Skype在线客服代码

    QQ,MSN,Skype在线客服代码 在网站建设时,为了更好的实施网站的营销型,会用到QQ,MSN等在线交流,以便客户能够快捷方便的联系我们.在这里,提供QQ,MSN的在线客服代码给大家分享: 1.Q ...

  10. Matlab与C/C++联合编程之Matlab以MEX方式调用C/C++代码(二)

    如果我有一个用C语言写的函数,实现了一个功能,如一个简单的函数: double add(double x, double y) { return x + y; } 现在我想要在Matlab中使用它,比 ...

随机推荐

  1. SQL 条件求和

    SUMIF 就是 Excel 中的 sumif () 函数的功能. 工作中用的频率极其高, 像我就几乎天天在用的呢. 也是做个简单的笔记而已. 为啥我总是喜欢对比 Excel 呢, 因为我也渐渐发现, ...

  2. Python基础 - 多进程(下)

    上篇主要对多任务从生活上来认识, 同时引入对 进程 的认识, 即操作系统资源分配的基本单元. 然后通过对 并发, 并行 概念的认识, 去理解 任务调度. 然后用内置的 multiprocessing ...

  3. OpenIddict使用教程

    OpenIddict是一个ASP.NET Core身份验证库,可帮助您添加OpenID Connect和OAuth 2.0支持到ASP.NET Core应用程序中.下面是OpenIddict使用教程的 ...

  4. 怎么查看软件保存的密码(WIN11)

    今天在用 Xshell 的时候,发现不记得密码了,虽然 Xshell 保存了密码,但是无法查看,都是星星,于是网上找了下,找到一篇文章,正好解决了我的困扰,里面有说明了实现原理,链接如下:https: ...

  5. codeup之Day of Week(给定日期判断周几

    题目描述 We now use the Gregorian style of dating in Russia. The leap years are years with number divisi ...

  6. Spring Boot 使用Apollo动态调整日志级别

    摘要:在Spring Boot 项目中,借助Apollo动态修改配置的能力,结合Logback修改日志级别打印执行的SQL脚本. 综述   在生产环境偶现测试环境未发现的SQL查询BUG,但由于线上关 ...

  7. CF1988D The Omnipotent Monster Killer

    CF1988D The Omnipotent Monster Killer 本文同步于我的网站. Problem 怪物们在一棵有 \(n\) 个顶点的树上,编号为 \(i(1\le i\le n)\) ...

  8. 洛谷 SP7258 SUBLEX - Lexicographical Substring Search

    洛谷 SP7258 SUBLEX - Lexicographical Substring Search Problem 先给你一个字符串s,后有T次询问.询问这个字符串的所有本质不同的子串中第k小的子 ...

  9. 「Log」CSP-S 2023 游记

    Day 0 什么题也没写,稍微复习了一下,晚上打了些板子. 整个人处于放空状态. Day 1 早上睡了懒觉,老爹早就给我点了肯德基早餐. 边吃早餐边看番,吃完了去群里水了一水,讨论了点杂七杂八的东西, ...

  10. C# 模式匹配全解:原理、用法与易错点

    引言 随着C#不断发展,"模式匹配"(Pattern Matching)已经成为让代码更加友好.可读和强大的核心特性.从 C# 7.0 初次引入,到 C# 11的能力扩展,模式匹配 ...