【并发编程】深入了解volatile

[](()二、防止指令重排

我们再来看指令重排。

[](()1、定义

指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序

介绍指令重排之前,首先介绍一下内存交互操作的8种指令吧。虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

| 指令 | 内容 |

| — | — |

| lock (锁定) | 作用于主内存的变量,把一个变量标识为线程独占状态 |

| read (读取) | 作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用 |

| load (载入) | 作用于工作内存的变量,它把read操作从主存中得到变量放入工作内存的变量副本中 |

| use (使用) | 作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作 |

| assign (赋值) | 作用于工作内存中的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |

| store (存储) | 作用于工作内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用 |

| write  (写入) | 作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中 |

| unlock (解锁) | 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 |

| 如图所示: | |

既然操作可以被分解为很多步骤, 那么多条操作指令就不一定依次序执行,因为每次只执行一条指令, 依次执行效率太低了。就像小时候学习的煮饭烧水任务时间分配一样,内存也会很聪明的分配时间。

本来想给大家整一个指令重排序的例子的,但是不管是我自己写还是用别人的代码,我的电脑都没办法让它重排序。但是我们都知道,指令重排是确实存在的(CPU确实会进行重排序,但是这种重排序是无法被我们观测到和控制的)。

一般重排序可以分为如下三种:

  • 1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

  • 2、指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

  • 3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

[](()2、原理

我们来看加了volatile前后的代码,用的就是阿里规约提供给我们的双重检查锁的代码。我们分别编译了两次,第一个是没有使用volatile关键字修饰的,第二个是使用volatile关键字来修饰,然后取出他们的的汇编代码(实在是设计的地方太底层,其实这里算是用到了策略模式了)

未使用volatile修饰

0x000000010d29e93b: mov %rax,%r10

0x000000010d29e93e: shr $0x3,%r10

0x000000010d29e942: mov %r10d,0x68(%rsi)

0x000000010d29e946: shr $0x9,%rsi

0x000000010d29e94a: movabs $0xfe403000,%rax

0x000000010d29e954: movb $0x0,(%rsi,%rax,1)

使用volatile修饰

0x0000000114353959: mov %rax,%r10

0x000000011435395c: shr $0x3,%r10

0x0000000114353960: mov %r10d,0x68(%rsi)

0x0000000114353964: shr $0x9,%rsi

0x0000000114353968: movabs $0x10db6e000,%rax

0x0000000114353972: movb $0x0,(%rsi,%rax,1)

0x0000000114353976: lock addl $0x0,(%rsp)

很明显,在movb操作后,加了volatile修饰的汇编代码后面多了一条汇编指令lock addl $0x0,(%rsp),这个操作相当于一个内存屏障,指令重排时不能把后面的指令重排序到内存屏障之前的位置。lock前缀会强制执行原子操作,它的作用是是的本CPU的cache写入了内存,该写入动作会引起别的CPU无效化其cache。所以通过这样一个空操作,可让前面volatile变量的便是对其他CPU可见

从硬件架构上讲,指令重排序是指CPU将多条指令不按程序规定的顺序分开发送给各相应的点,但并不是指令任意重排,CPU需要能正确处理指令,以保障程序能得出正确的执行结果。lock addl $0x0,(%rsp) 指令把修改同步到内存时,意味着所有值钱的操作都已经执行完成,这样便形成了指令重排序无法越过内存屏障的效果。

[](()三、内存屏障

既然指令重排和可见性都依赖了lock,同时lock指令引出了内存屏障,我们就来学习一下什么是内存屏障。

[](()1、定义

内存屏障:保证屏障前的读写指令必须在屏障后的读写指令之前执行,通知被Volatile修饰的值,每次读取都从主存中读取,每次写入都同步写入主存。

内存屏障具体又分为写屏障和读屏障 写屏障(Store Memory Barrier):强制将缓存中的内容写入到缓存中或者将该指令之后的写操作写入缓存直到之前的内容被刷入到缓存中,也被称之为smp_wmb 读屏障(Load Memory Barrier):强制将无效队列(volatile写操作之后失其作废)中的内容处理完毕,也被称之为smp_rmb

| 屏障类型 | 指令示例 | 说明 |

| — | — | — |

| LoadLoadBarriers | Load1;LoadLoad;Load2 | 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作 |

| StoreStoreBarriers | Store1;StoreStore;Store2 | 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作 |

| | | |

| LoadStoreBarriers | Load1;LoadStore;Store2 | 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作 |

| | | |

| StoreLoadBarriers | Store1;StoreLoad;Load1 | 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作.它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令 |

[](()2、原理

内存屏障在Java中的体现

  • 1、volatile读之后,所有变量读写操作都不会重排序到其前面。

  • 2、volatile读之前,所有volatile读写操作都已完成。

  • 3、volatile写之后,volatile变量读写操作都不会重排序到其前面。

  • 4、volatile写之前,所有变量的读写操作都已完成。

根据JMM规则,结合内存屏障的相关分析得出以下结论

  • 1、在每一个volatile写操作前面插入一个StoreStore屏障。这确保了在进行volatile写之前前面的所有普通的写操作都已经刷新到了内存。

  • 2、在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的volatile读写操作发生重排序。

  • 3、在每一个volatile读操作后面插入一个LoadLoad屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。

  • 4、在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。

如下图所示:

[](()3、as-if-serial语义

但是用了volatile关键字,程序的运行速度必然会受到影响,那么除了volatile关键字以外什么时候不会发生重排序呢?这里就要引入as-if-serial语义。

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。

如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,者三种操作都是存在数据依赖性 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】。如果重排序会对最终执行结果会存在影响,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序。

int a=1;

int b=2;

int c =a+b;

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。比如上面计算的代码,在单线程中,会让人感觉代码是一行一行顺序执行上,实际上a,b两行不存在数据依赖性可能会进行重排序,即a,b不是顺序执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题。

说到底,as-if-serial语义不过是一种最基础的架构定义,可以类比地球上氧气的比例约为21%。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值