处理器解决MESI协议带来写请求阻塞问题的方案:
(1)引入store buffer
将同步等待变成异步的,单独为写操作划分一个store buffer,这样读的数据完全由cache 获取。而store buffer只负责写数据。 CPU可以先将要写入的数据写到Store Buffer,然后继续做其它事情。等到收到其它CPU发过来的Cache Line(Read Response),再将数据从Store Buffer移到Cache Line。结构如下所示:

然后加了Store Buffer之后,会引入另一个问题:cache 的数据是不准的,因为store buffer数据还没同步到cache,store buffer 只负责写,取数从cache 里面取出来
a = 2;
b = a + 1 ;
初始状态下,假设a,b值都为0,并且a存在CPU1的Cache Line中(Shared状态),可能出现如下操作序列:
- CPU0 要写入A,发出Read Invalidate消息,并将a=1写入Store Buffer
- CPU1 收到Read Invalid,返回Read Response(包含a=0的Cache Line)和Invalid Ack
- CPU0 收到Read Response,更新Cache Line(a=0)
- CPU0 开始执行 b = a + 1,从Cache Line中加载a,得到a=0 ,然后此时 b的值是1,跟我们预测的 b =2+1 是违背。
- CPU0 收到所有的invalid ack,将Store Buffer中的a=1应用到Cache Line
造成原因: 同一个CPU存在对a的两份拷贝,一份在Cache,一份在Store Buffer,前者用于读,后者用于写,因而出现单线程情况下CPU执行顺序与程序顺序(Program Order)不一致(看起来是先执行了b=a+1,再执行a=1)。
科普一下:
as-if-serial语义:不管怎么重排(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变,不会对存在数据依赖关系的操作进行重排序,编译器,runtime和处理器必须遵守as-if-serial 语义。
- 真·重排序:编译器,底层硬件cpu等(指令级别),出于“优化”的目的,按照某种规则将指令重新排序。
- 假·重排序:由于store buffer 缓存同步cache
的顺序问题,看起来指令被重排序了。但是在语义上面是不允许被重排序的,因为存在关联关系。
Store Forwarding 技术:
CPU可以直接从Store Buffer中加载数据,即支持将CPU存入Store Buffer的数据传递(forwarding)给后续的加载操作,而不经由Cache。(为了解决同一cpu里面cache 和store buffer 数值不一致)

(2)Invalid Queue:将同步响应Invalid ack 变成异步
Invalid Ack耗时的主要原因是CPU要先将对应的Cache Line置为Invalid后再返回Invalid Ack,一个很忙的CPU可能会导致其它CPU都在等它回Invalid Ack。
解决思路还是化同步为异步 : CPU不必要处理了Cache Line之后才回Invalid Ack,而是可以先将Invalid消息放到某个请求队列Invalid Queue,然后就返回Invalid Ack。CPU可以后续再处理Invalid Queue中的消息,大幅度降低Invalid Ack响应时间。
此时的CPU Cache结构图如下:

- 对于所有的收到的 Invalidate 请求,Invalidate Acknowlege 消息必须立刻发送 Invalidate
- 并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。
加了lnvalid queue之后,会引入另一个问题:cache 的数据是不准的,因为lnvalid queue数据还没同步到cache,从cache 读出来还是不准的数据。

回到我们一开始的问题,在改良后的cpu 缓存模型下,为什么thread还看不到flag 变量的修改?
是因为有延迟,store buffer 异步等待 其他cpu 的valid ack,才能将cache变成exclusive 并且修改变成modify。
- main 线程发出invalid 信号,等待 thread 信号响应 invalid ack 信号,thread 收到invalid
信号,把cache设置为Invalid。 然后返回Invalid ack 信号

- 但其实我们main程序还没等到invalid ack 就结束了,根本就没有修改到memory 的值,thread 因为cache Invalid 只能从memory 获取到旧的值

public class NoVolatile {
private boolean flag = true;
public void test() {
System.out.println("start");
while (flag) {
}
System.out.println("end");
}
public static void main(String[] args) {
NoVolatile noVolatile = new NoVolatile();
new Thread(noVolatile::test).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
noVolatile.flag = false;
}
}
//执行结果:
start
那么怎样才能看到thread 线程在不通过validate的情况下能获取到值呢? 将 main 放久一些,就能得到这个效果。
public class NoVolatile {
private boolean flag = true;
public void test() {
System.out.println("start");
System.out.println(flag);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
System.out.println(flag);
System.out.println("end");
}
public static void main(String[] args) {
NoVolatile noVolatile = new NoVolatile();
new Thread(noVolatile::test).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
noVolatile.flag = false;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {

本文深入探讨了Volatile关键字在Java中的实现原理,包括解决可见性问题的MESI协议、StoreBuffer与InvalidQueue机制,以及如何通过内存屏障解决重排序问题。文章还详细解释了Volatile如何保证内存可见性与有序性,以及它为何不能保证原子性。
最低0.47元/天 解锁文章
1432





