请看下面的代码并尝试猜测输出:

可能一看下面的代码你可能会放弃继续看了,但如果你想要彻底弄明白volatile,你需要耐心,下面的代码很简单!

在下面的代码中,我们定义了4个字段x,y,a和b,它们被初始化为0

然后,我们创建2个分别调用Test1和Test2的任务,并等待两个任务完成。

完成两个任务后,我们检查a和b是否仍为0,

如果是,则打印它们的值。

最后,我们将所有内容重置为0,然后一次又一次地运行相同的循环。

  1. using System;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4. namespace MemoryBarriers
  5. {
  6. class Program
  7. {
  8. static volatile int x, y, a, b;
  9. static void Main()
  10. {
  11. while (true)
  12. {
  13. var t1 = Task.Run(Test1);
  14. var t2 = Task.Run(Test2);
  15. Task.WaitAll(t1, t2);
  16. if (a == 0 && b == 0)
  17. {
  18. Console.WriteLine("{0}, {1}", a, b);
  19. }
  20. x = y = a = b = 0;
  21. }
  22. }
  23. static void Test1()
  24. {
  25. x = 1;
  26. // Interlocked.MemoryBarrierProcessWide();
  27. a = y;
  28. }
  29. static void Test2()
  30. {
  31. y = 1;
  32. b = x;
  33. }
  34. }

如果您运行上述代码(最好在Release模式下运行),则会看到输出为0、0的许多输出,如下图。

我们先根据代码自我分析下

在Test1中,我们将x设置为1,将a设置为y,而Test2将y设置为1,将b设置为x

因此这4条语句会在2个线程中竞争

罗列下可能会发生的几种情况:

1. Test1先于Test2执行:

  1. x = 1
  2. a = y
  3. y = 1
  4. b = x

在这种情况下,我们假设Test1在Test2之前完成,那么最终值将是

  1. x = 1a = 0y = 1b = 1

2. Test2执行完成后执行Test1:

  1. y = 1
  2. b = x
  3. x = 1
  4. a = y

在这种情况下,那么最终值将是

  1. x = 1a = 1y = 1b = 0

2. Test1执行期间执行Test2:

  1. x = 1
  2. y = 1
  3. b = x
  4. a = y

在这种情况下,那么最终值将是

  1. x = 1a = 1y = 1b = 1

3. Test2执行期间执行Test1

  1. y = 1
  2. x = 1
  3. a = y
  4. b = x

在这种情况下,那么最终值将是

  1. x = 1a = 1y = 1b = 1

4. Test1交织Test2

  1. x = 1
  2. y = 1
  3. a = y
  4. b = x

在这种情况下,那么最终值将是

  1. x = 1a = 1y = 1b = 1

5.Test2交织Test1

  1. y = 1
  2. x = 1
  3. b = x
  4. a = y

在这种情况下,那么最终值将是

  1. x = 1a = 1y = 1b = 1

我认为上面已经罗列的

已经涵盖了所有可能的情况,

但是无论发生哪种竞争情况,

看起来一旦两个任务都完成,

就不可能使a和b都同时为零,

但是奇迹般地,居然一直在打印0,0 (请看上面的动图,如果你怀疑的话代码copy执行试试)

真相永远只有一个

先揭晓答案:cpu的乱序执行

让我们看一下Test1和Test2的IL中间代码。

我在相关部分中添加了注释。

  1. #ConsoleApp9.Program.Test1()
  2. #function prolog ommitted
  3. L0015: mov dword ptr [rax+8], 1 # 把值 1 上传到内存地址 'x'
  4. L001c: mov edx, [rax+0xc] # 从内存地址 'y' 下载值并放到edx(寄存器)
  5. L001f: mov [rax+0x10], edx. # 从(edx)寄存器把值上传到内存地址 'a'
  6. L0022: add rsp, 0x28.
  7. L0026: ret
  8. #ConsoleApp9.Program.Test2()
  9. #function prolog
  10. L0015: mov dword ptr [rax+0xc], 1 # 把值 1 上传到内存地址 'y'
  11. L001c: mov edx, [rax+8]. # 从内存地址 'x' 下载值并放到edx(寄存器)
  12. L001f: mov [rax+0x14], edx. # 从(edx)寄存器把值上传到内存地址 'b'
  13. L0022: add rsp, 0x28
  14. L0026: ret

请注意,我在注释中使用“上载”和“下载”一词,而不是传统的读/写术语。

为了从变量中读取值并将其分配到另一个存储位置,

我们必须将其读取到CPU寄存器(如上面的edx),

然后才能将其分配给目标变量。

由于CPU操作非常快,因此与在CPU中执行的操作相比,对内存的读取或写入真的很慢。

所以我使用“上传”和“下载”,相对于CPU的高速缓存而言【读取和写入内存的行为】

就像我们向远程Web服务上载或从中下载一样慢。

以下是各项指标(2020年数据)(ns为纳秒)

L1 cache reference: 1 ns

L2 cache reference: 4 ns

Branch mispredict: 3 ns

Mutex lock/unlock: 17 ns

Main memory reference: 100 ns

Compress 1K bytes with Zippy: 2000 ns

Send 2K bytes over commodity network: 44 ns

Read 1 MB sequentially from memory: 3000 ns

Round trip within same datacenter: 500,000 ns

Disk seek: 2,000,000 ns

Read 1 MB sequentially from disk: 825,000 ns

Read 1 MB sequentially from SSD: 49000 ns

由此可见 访问主内存比访问CPU缓存中的内容慢100倍

如果让你开发一个应用程序,实现上载或者下载功能。

您将如何设计此?肯定想要开多线程,并行化执行以节省时间!

这正是CPU的功能。CPU被我们设计的很聪明,

在实际运行中可以确定某些“上载”和“下载”操作(指令)不会互相影响,

并且CPU为了节省时间,对它们(指令)进行了(优化)并行处理,

也叫【cpu乱序执行】(out-of-order)

上面我说道:在实际运行中可以确定某些“上载”和“下载”操作(指令)不会互相影响,

这里有一个前提条件哈:该假设仅基于基于线程的依赖性检查进行(per-thread basis dependency checks)。

虽然在单个线程是可以被确定为指令独立性,但CPU无法考虑多个线程的情况,所以提供了【volatile关键字】

我们回到上面的示例,尽管我们已将字段标记为volatile,但感觉上没有起作用。为什么?

一般说道volatile我都一般都会举下面的例子(内存可见性)

  1. using System;
  2. using System.Threading;
  3. public class C {
  4. bool completed;
  5. static void Main()
  6. {
  7. C c = new C();
  8. var t = new Thread (() =>
  9. {
  10. bool toggle = false;
  11. while (!c.completed) toggle = !toggle;
  12. });
  13. t.Start();
  14. Thread.Sleep (1000);
  15. c.completed = true;
  16. t.Join(); // Blocks indefinitely
  17. }
  18. }

如果您使用release模式运行上述代码,它也会无限死循环。

这次CPU没有罪,但罪魁祸首是JIT优化。

你如果把:

  1. bool completed;

改成

  1. volatile bool completed;

就不会死循环了。

让我们来看一下[没有加volatile]和[加了volatile]这2种情况的IL代码:

没有加volatile

  1. L0000: xor eax, eax
  2. L0002: mov rdx, [rcx+8]
  3. L0006: movzx edx, byte ptr [rdx+8]
  4. L000a: test edx, edx
  5. L000c: jne short L001a
  6. L000e: test eax, eax
  7. L0010: sete al
  8. L0013: movzx eax, al
  9. L0016: test edx, edx # <-- 注意看这里
  10. L0018: je short L000e
  11. L001a: ret

加了volatile

  1. L0000: xor eax, eax
  2. L0002: mov rdx, [rcx+8]
  3. L0006: cmp byte ptr [rdx+8], 0
  4. L000a: jne short L001e
  5. L000c: mov rdx, [rcx+8]
  6. L0010: test eax, eax
  7. L0012: sete al
  8. L0015: movzx eax, al
  9. L0018: cmp byte ptr [rdx+8], 0 <-- 注意看这里
  10. L001c: je short L0010
  11. L001e: ret

留意我打了注释的那行。上面的这些IL代码行 实际上是代码进行检查的地方:

  1. while (!c.completed)

当不使用volatile时,JIT将完成的值缓存到寄存器(edx),然后仅使用edx寄存器的值来判断(while (!c.completed))。

但是,当我们使用volatile时,将强制JIT不进行缓存,

而是每次我们需要读取它直接访问内存的值 (cmp byte ptr [rdx+8], 0)

JIT缓存到寄存器 是因为 发现了 内存访问的速度慢了100倍以上,就像CPU一样,JIT出于良好的意图,缓存了变量。

因此它无法检测到别的线程中的修改。

volatile解决了这里的问题,迫使JIT不进行缓存。

说完可见性了我们在来说下volatile的另外一个特性:内存屏障

  1. 确保在执行下一个上传/下载指令之前,已完成从volatile变量的下载指令。

  2. 确保在执行对​​volatile变量的当前上传指令之前,完成了上一个上传/下载指令。

但是volatile并不禁止在完成上一条上传指令之前完成对volatile变量的下载指令。

CPU可以并行执行并可以继续执行任何先执行的操作。

正是由于volatile关键字无法阻止,所以这就是这里发生的情况:

  1. mov dword ptr [rax+0xc], 1 # 把值 1 上传到内存地址 'y'
  2. mov edx, [rax+8]. # 从内存地址 'x' 下载值并放到edx(寄存器)

变成这个

  1. mov edx, [rax+8]. # 从内存地址 'x' 下载值并放到edx(寄存器)
  2. mov dword ptr [rax+0xc], 1 # 把值 1 上传到内存地址 'y'

因此,由于CPU认为这些指令是独立的,因此在y更新之前先读取x,同理在Test1方法也是会发生x更新之前先读取y。

所以才会出现本文例子的坑~~!

如何解决?

输入内存屏障 内存屏障是对CPU的一种特殊锁定指令,它禁止指令在该屏障上重新排序。因此,该程序将按预期方式运行,但缺点是会慢几十纳秒。

在我们的示例中,注释了一行代码:

  1. //Interlocked.MemoryBarrierProcessWide();

如果取消注释该行,程序将正常运行~~~~~

总结

平常我们说volatile一般很容易去理解它的内存可见性,很难理解内存屏障这个概念,内存屏障的概念中对于volatile变量的赋值,

volatile并不禁止在完成上一条上传指令之前完成对volatile变量的下载指令。这个在多线程环境下一定得注意!

volatile的内存屏障的坑的更多相关文章

  1. volatile 和 内存屏障

    接下来看看volatile是如何解决上面两个问题的: 被volatile修饰的变量在编译成字节码文件时会多个lock指令,该指令在执行过程中会生成相应的内存屏障,以此来解决可见性跟重排序的问题. 内存 ...

  2. synchronized 与 volatile 原理 —— 内存屏障的重要实践

    单例模式的双重校验锁的实现: 第一种: private static Singleton _instance; public static synchronized Singleton getInst ...

  3. C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

    目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...

  4. 【C#】通过一个案例 彻底了解 Volatile和 内存屏障

    案例如下的.我个人理解是不会出现出现0,0的结果,但是很明显出现了. 说明对我对 Volatile\内存屏障\乱序排序的理解是不对. 今天就通过这个案例,理清这些概念. using System; u ...

  5. JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法

    JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...

  6. volatile关键字?MESI协议?指令重排?内存屏障?这都是啥玩意

    一.摘要 三级缓存,MESI缓存一致性协议,指令重排,内存屏障,JMM,volatile.单拿一个出来,想必大家对这些概念应该有一定了解.但是这些东西有什么必然的联系,或者他们之间究竟有什么前世今生想 ...

  7. java内存屏障

    为什么会有内存屏障 每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取.但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的 ...

  8. 内存屏障在CPU、JVM、JDK中的实现

    前言 内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行, 也就是说在内 ...

  9. [面试]volatile类型修饰符/内存屏障/处理器缓存

    volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...

随机推荐

  1. LocalDateTime去掉T

    最近在使用阿里巴巴的fastjson反序列化对象的时候,对象里面时间格式属性总是会多了一个T  2021-1-09T18:29:09.097 这个T是啥国际标准,但是我们的前端又不需要这个T,所以就要 ...

  2. 华为路由配置IPSec

    用该方法配置后用抓包工具抓取的就看不到两个通讯点的IP,而显示的是加密点的IP. 原文:https://www.cnblogs.com/yangyang1988/p/11559819.html

  3. GIT常用命令:

    1.安装好Git之后,点击鼠标右键即可看到有Git bush选项,点击即可进入Git命令行操作. 2.使用命令: git  config  --global user.name "lyh&q ...

  4. pickle — Python object serialization

    pickle - Python object serialization  消息队列

  5. https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf 检验hash冲突

    https://github.com/google/cityhash We like to test hash functions with SMHasher, among other things. ...

  6. HTTPS学习(二):原理与实践

    div.example { background-color: rgba(229, 236, 243, 1); color: rgba(0, 0, 0, 1); padding: 0.5em; mar ...

  7. 【Oracle】SQL/92 执行多个表的连接

    内连接 外连接 自连接 交叉连接 1.内连接 表名 INNER JOIN 表名 ON 条件 等价于: FROM 表名, 表名 WHERE 条件 SELECT p.name, pt.name, pt.p ...

  8. linux下mysql基于mycat做主从复制和读写分离之基础篇

    Linux下mysql基于mycat实现主从复制和读写分离1.基础设施 两台虚拟机:172.20.79.232(主) 172.20.79.233(从) 1.1软件设施 mysql5.6.39 , my ...

  9. yuan先生博客地址

    1 Web应用  https://www.cnblogs.com/yuanchenqi/articles/8869302.html2 http协议 https://www.cnblogs.com/yu ...

  10. Django(视图)

    一个视图函数,简称视图,是一个简单的Python 函数,它接受Web请求并且返回Web响应.响应可以是一张网页的HTML内容,一个重定向,一个404错误,一个XML文档,或者一张图片. . . 是任何 ...