本文主要是介绍MESI与Volatile(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
MESI缓存一致性协议
定义:
多核CPU的情况下有多个一级缓存,一致性协议MESI保证缓存内部数据的一致,不让系统数据混乱。
缓存行(Cache line):缓存存储数据的单元
缓存行状态:
状态 | 描述 | 监听任务 |
---|---|---|
M 修改 (Modified) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对于主存的操作。这种操作必须在该缓存行数据写回主存并将状态变成S(共享)状态之后执行。 |
E 独享、互斥 (Exclusive) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读该缓存行相对于主存的操作。一旦有这种操作,该缓存行需要变成S(共享)状态。 |
S 共享 (Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
I 无效 (Invalid) | 该Cache line无效。 | 无 |
注意:
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。
一个缓存行不能确保其他缓存行的数据都已作废,因为其它缓存不会广播他们作废掉该缓存行的通知,所以不会从 S 转变为 E
一个Cache line所处的状态 和 另外一个cache line 需要调整的状态
M | E | S | I | |
---|---|---|---|---|
M | × | × | × | √ |
E | × | × | × | √ |
S | × | × | √ | √ |
I | √ | √ | √ | √ |
举个栗子来说:
假设Cache 1 中有一个变量x = 0的Cache line 处于S状态(共享)
那么其他拥有x变量的Cache 2、Cache 3等x的Cache line调整为S状态(共享)或者调整为 I 状态(无效),它们不能调整为M 或 E
volatile关键字
定义:
volatile是JVM提供的轻量级的同步机制。volatile关键字有如下两个作用:
- 保证被volatile修饰的共享变量对所有线程总是可见的
- 禁止指令重排序优化
volatile可见性
volatile关键字的作用是保证可见性,通过在指令前添加汇编前缀lock
,将当前处理器缓存行的数据写回到系统内存,并触发硬件缓存锁定机制
硬件缓存锁定机制
- 触发总线锁 —— Cache line容量不足【效率低】
- 触发MESI缓存一致性协议
volatile有序性
volatile关键字另一个作用就是**禁止指令重排优化,**从而避免多线程环境下程序出现乱序执行的现象,volatile是通过 内存屏障 实现禁止指令重排优化
内存屏障
定义:
内存屏障,又称内存栅栏,是一个CPU指令。作用有两个
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
JVM中提供了四类内存屏障指令:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
作用:
在指令间插入一条Memory Barrier
- 告诉编译器和CPU,任何指令都不能和这条Memory Barrier指令重排序
- 强制刷出CPU的各种缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
指令重排序导致问题 —— DCL例子
public class DoubleCheckLock {private volatile static DoubleCheckLock instance;private DoubleCheckLock(){}public static DoubleCheckLock getInstance(){//第一次检测if (instance==null){//同步synchronized (DoubleCheckLock.class){if (instance == null){//多线程环境下可能会出现问题的地方instance = new DoubleCheckLock();}}}return instance;}
}
// 对象实例化的三个步骤
memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null/* 重排 */memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象/**
解释:
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的!当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题
*/
volatile内存语义实现
对于加了 volatile 的共享变量,JMM针对编译器制定 volatile 重排序的规则
第一个操作 | 第二个操作:普通读写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
---|---|---|---|
普通读写 | 可以重排 | 可以重排 | 不可以重排 |
volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
JMM规定:
- volatile写 不可以重排在 任何操作 之前
- volatile读 不可以重排在 任何操作 之后
- volatile读 不可以重排在 volatile写 之前
例子
class VolatileBarrierExample {int a;volatile int v1 = 1;volatile int v2 = 2;void readAndWrite() {int i = v1; // 第一个volatile读int j = v2; // 第二个volatile读a = i + j; // 普通写v1 = i + 1; // 第一个volatile写v2 = j * 2; // 第二个volatile写}
}
volatile不保证原子性
自增操作例子
自增操作在底层执行的操作不是原子的,分为:
- Load 数据
- Increment
- Store 刷回主存
100自增
当线程A执行到第2步后【Increment -> 101】,线程B执行完第3步操作【Store -> 101】并通知线程A缓存失效,因为线程A已经执行完第1步和第2步了,所以造成了数据丢失。
这篇关于MESI与Volatile(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!