【并发编程】MESI缓存一致性协议

一、volatile关键字具有可见性

在以下例子中,可以发现threadA线程无法感知到flag的变化,导致“线程threadA感知到flag的改变”这行代码一直无法在控制台打印出来。但是,如果使用volatile来修饰flag后发现threadA可以感应到flag的变化,但是底层又是怎么实现的呢?

public class TestVisibility {

    private static boolean flag = false;

    public static void main(String[] args){
        Thread threadA = new Thread(()->{
            while (!flag){}
            System.out.println("线程:" + Thread.currentThread().getName()
                    + "感知到flag的改变");
        },"threadA");
        threadA.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread threadB = new Thread(()->{
            System.out.println("start.......");
            flag = true;
            System.out.println("end.......");
        },"threadB");
        threadB.start();
    }

}

CPU的工作模式:内存中的数据 -> 生成数据副本(不同的线程生成各自的数据副本,互不影响) -> bus总线 -> 缓存
在这里插入图片描述

从流程图来看,threadA和threadB对flag的操作是相互隔离的,这就是为什么threadA线程无法感知到flag的变化。但是当threadB线程将flag的取值写回到内存后,threadA还是有可能感知到flag的变化的(怎么感知?答:由于CPU缓存的最小单位是缓存行,一般是64KB,当flag所在的缓存行中有数据过期需要重新从内存中获取数据的时候,就会将整个缓存行失效,这样也就可以读取到最新的flag取值了。只不过这种情况就有一定的随机性和延迟性了)。

threadA如何能够立刻感知到flag的变化呢?
1>总线锁
2>MESI缓存一致性协议

1>总线锁
在这里插入图片描述

2>MESI缓存一致性协议(添加volatile来修饰flag)
在这里插入图片描述
第一步:当threadA第一次去查询flag的时候,会将副本信息通过总线传递给CPU,此时数据状态为E(独享);
第二步:当threadB去查询flag的时候,发现threadA在使用该字段,则生成的副本信息中数据状态为S(共享),并将threadA中的数据状态改为S;
第三步:当threadA和threadB同时对flag进行操作的时候,总线会通过总线裁决来决定哪个线程先执行。假如threadB获取了执行权,它就会和总线要数据了,将它对应的数据副本加载到CPU缓存中。当threadB要进行数据修改的时候会将数据状态由S改为M(修改)并会通过总线通知threadA将数据状态由S改为I(失效);
第四步:threadB会将修改后的数据存到CPU的StoreBuffer中,由于通知其它副本将状态改为I需要经过总线操作,相对耗时。给人的感觉就是一失效,我就可以从StoreBuffer中获取到最新的数据了,就可以将失效状态改为S了;
第五步:StoreBuffer中的数据什么时候会通过总线写会内存,这个时间是不确定的。

二、volatile关键字为什么不具有原子性

例如:counter++
在这里插入图片描述

第一步:当threadA第一次去查询counter的时候,会将副本信息通过总线传递给CPU,此时数据状态为E(独享);
第二步:当threadB去查询counter的时候,发现threadA在使用该字段,则生成的副本信息中数据状态为S(共享),并将threadA中的数据状态改为S;
第三步:当threadA和threadB同时对counter进行操作的时候,总线会通过总线裁决来决定哪个线程先执行。假如threadB获取了执行权,它就会和总线要数据了,将它对应的数据副本加载到CPU缓存中。当threadB要进行数据修改的时候会将数据状态由S改为M(修改)并会通过总线通知threadA将数据状态由S改为I(失效);
第四步:threadB会将修改后的数据存到CPU的StoreBuffer中,由于通知其它副本将状态改为I需要经过总线操作,相对耗时。给人的感觉就是一失效,我就可以从StoreBuffer中获取到最新的数据了,就可以将失效状态改为S了;
第五步:StoreBuffer中的数据什么时候会通过总线写会内存,这个时间是不确定的。

threadA中标志为I的命令是怎么处理的呢?
1>直接丢弃,从store buffer获取到最新的值,不需要再写回内存;
2>继续执行完当前指令,此时counter为0,则继续执行counter = counter+1,结果为1,写回内存。这样就相当于threadA和threadB都写回到了主内存中,counter的取值都为1。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值