volatile

作用

  • 保证可见性
  • 保证有序性

CPU 的简单缓存模型

在这里插入图片描述

每个 CPU 都有自己的多级缓存,越靠近 CPU,容量越小,价格越贵,速度越快。现在的CPU 一般都有三级。在多线程环境下,不同的 CPU 缓存之间容易导致数据不一致的问题。解决方案:

  • 总线加锁机制:CPU读写数据之前,通过总线对某个数据加锁。其他CPU不能碰,所以容易导致串行化,效率低,现已废弃。
  • 缓存一致性协议:其中一种叫 MESI 协议,现在 volatile 的底层原理,简单说就是让缓存及时过期

CPU 缓存的结构

CPU 的缓存结构类似于 JDK1.7 HashMap,是一个拉链散列结构。散列表中每个元素称为一个桶,bucket,每个桶可以通过拉链法挂着多个缓存条目,cache entry,每个缓存条目包含三部分信息:

  • tag:标签,指向缓存数据在主存中的地址
  • cache line:缓存行,缓存数据的值,可以包含多个数据,但对应主存地址必须连续
  • flag:标记位,标记缓存条目的状态

在这里插入图片描述

CPU 怎么找到缓存中的数据 ?根据变量名,进行解码,得到三部分信息:

  • index:散列表中的桶下标
  • tag:定位到哪个缓存条目
  • offset:缓存行的偏移量(因为缓存行可能有多个数据,比如数组)

根据这些信息就可以定位到缓存中的数据。

缓存条目的标记位取值:

  • I:即 invalid,对应的缓存行的数据已经过期
  • S:即shared,各个CPU共享这份数据
  • E:即 exclusive,当前CPU正在修改,所以独占这份数据
  • M:即 modified,当前CPU修改完了,未刷入主存

所以叫MESI协议。

CPU 的总线嗅探机制

每个CPU都有自己的缓存,就意味着任意一份数据都可能有多份副本。假如 CPU-1 修改了自己缓存中的数据,其他 CPU 读写自己缓存中的数据时,就可能读到过期的数据,所以引入总线嗅探机制:
CPU-1 准备修改缓存中的数据时,要先发送一个无效消息 invalidate到总线,相当于广播,告诉其他CPU,我准备修改某个数据,你们要那个数据标记为无效,即 flag=I
CPU 的总线嗅探机制会检测到总线有 invalidate 消息,然后看自己的缓存中是否有这个数据,有就标记为无效,并且发送一条无效确认消息 invalidate ack 到总线;
CPU-1 发送无效消息出去之后,要从总线嗅探到其他所有CPU的无效确认,将自己缓存的数据标记为 E,然后修改,再标记为 M,相当于加锁,写操作,释放锁。至于要不要进一步刷入主存,何时刷入,不同的硬件有不同实现。
其他CPU读取数据时发现缓存过期了,就去主存或者CPU-1的缓存中加载。

CPU-1 修改变量 i 的流程:

在这里插入图片描述

写缓存区与无效队列的引入

每次修改数据都要发一个无效消息,等到其他所有CPU返回一个无效确认,才能修改,阻塞时间太长了,所以引入了写缓存区和无效队列,每个CPU都有自己的写缓冲区和无效队列。
写数据时,直接发送一条无效消息,然后直接写入写缓存区中;
收到无效消息时,放入无效队列,就直接返回无效确认,等到CPU空闲时才消费无效队列里面的消息;
收到其他所有CPU的无效确认后,就将写缓冲区的数据刷入缓存,是否刷入主存取决于硬件实现。
CPU-1修改变量i的过程就变成这样:
在这里插入图片描述

其中,第 7、8 步称为 flush 操作,第 9、10、11 步称为 reflush 操作。

缓存一致性协议会导致的问题

缓存一致性协议就是强制 CPU 的缓存更新后,强制刷入主存。在引入写缓冲区、无效队列之前,缓存一致性协议就可以保证 CPU 读取到最新的数据;但引入之后,修改后的数据可能留在写缓冲区,无效消息可能还在无效队列,未被消费,就导致了可见性的问题。
可见性问题的出现也会导致 CPU 执行的指令 “看起来”乱序,即所谓的内存重排序,比如CPU-1 先是修改缓存中的变量 a,再读取变量 b,但 flush 操作发生在读完变量 b 之后,所以“看起来”是先读后写。
另外一种发生重排序的可能:CPU 的指令级并行技术也会导致没有数据相关的指令乱序执行。
为解决这些问题,就引入了内存屏障。
使用 Store 屏障、Load 屏障来解决可见性问题,它们的作用如下:

  • Store:CPU修改数据后,强制CPU阻塞等待无效确认,然后执行 flush 操作;
  • Load:CPU读取数据之前,强制清空无效队列的消息。

使用 AcquireRelease来解决有序性问题,作用:

  • Acquire屏障:保证读指令、写指令的执行顺序与程序的顺序一致
  • Release屏障:写数据后强制 flush,读数据前强制清空无效队列,并且保证写指令的按序执行

有的书籍会把这些内存屏障称为 LoadLoadLoadStore等等,其实只是不同的硬件实现或者JVM版本,底层的原理都一样,都是在控制 flush操作和reflush操作的时机,或者规定指令执行的顺序。

Java 内存模型的简化

JMM 是一个标准化的、抽象的概念,屏蔽了缓存、写缓冲区、无效队列等硬件细节,简化成工作内存的概念。JMM 对数据的读写抽象成一些标准操作:

在这里插入图片描述

面试注意

volatile 关键字的作用 ?
保证可见行、有序性
原理?
MESI协议、内存屏障;
然后主动讲一下硬件层面的原理,顺序:
1、CPU 缓存的结构
2、MESI 协议在硬件层面的原理
3、为什么要引入写缓冲区、无效队列
4、为什么会产生可见行、有序性的问题
5、内存屏障的原理

常见误区

volatile 可以保证原子性
错,在特定的情况下,比如32位的JVM操作64位的volatile变量时,的确可以保证,但这不是 volatile的原子性语义。在绝大多数情况下,都无法保证。

volatile的底层原理就是禁用缓存

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值