机组学习笔记之存储器——volatile一词解读CPU“缓存模型”

这里先给出volatile的两大作用:

  • 1.实现线程之间的可见性
  • 2.防止编译时期的指令重排

这里我们会终点讲解volatile的第一个作用“可见性”,对于对于第二个作用大家可以参考这个连接

JMM 与计算机缓存模型

因为java的内存模型JMM与计算机中的缓存模型是极其相似的,这里我们根据一段代码直观的感受一下“可见性”;

public class VolatileTest {
    
    
    private static volatile int COUNTER = 0;
//    private static /*volatile*/ int COUNTER = 0;

    public static void main(String[] args) {
    
    
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    /**
     * 监听COUNTER变量,只要改变就打印出改变的值
     */
    static class ChangeListener extends Thread {
    
    
        @Override
        public void run() {
    
    
            int threadValue = COUNTER;
            while (threadValue < 5) {
    
    
                if (threadValue != COUNTER) {
    
    
                    System.out.println("Get change for COUNTER: " + COUNTER);
                    threadValue = COUNTER;
                }
            }
        }
    }

    static class ChangeMaker extends Thread {
    
    
        @Override
        public void run() {
    
    
            int threadValue = COUNTER;
            while (threadValue < 5) {
    
    
                System.out.println("Increment COUNTER to " + (threadValue + 1));
                COUNTER = ++threadValue;

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

简单解读下两个类,ChangeMaker 每隔5毫秒更新下COUNTER的值,ChangeListener 用来监控COUNTER的值,只要有更新就打印出来;第一次我们将COUNTER用volatile关键字,打印结果:

Increment COUNTER to 1
Get change for COUNTER: 1
Increment COUNTER to 2
Get change for COUNTER: 2
Increment COUNTER to 3
Get change for COUNTER: 3
Increment COUNTER to 4
Get change for COUNTER: 4
Increment COUNTER to 5
Get change for COUNTER: 5

我们发现ChangeMaker 每更新一次,ChangeListener就会监视到并进行打印
第二次我们将关键字volatile注释掉,结果如下:

Increment COUNTER to 1
Get change for COUNTER: 1
Increment COUNTER to 2
Increment COUNTER to 3
Increment COUNTER to 4
Increment COUNTER to 5

我们发现ChangeListener不是总是监视到COUNTER的变化,并且程序会一直卡在ChangeListener中
在这里插入图片描述
上面代码演示的通俗解释来说就是,我们在ChangeMaker线程中修改了COUNTER得值,volatile能够保证及时刷新会主内存中,同时在ChangeListener也会从主内存中读取。既保证变量在不同线程之间的可见性

java中volatile的可见性取决于JVM的支持,可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。
计算机内部的缓存写会策略包括写直达和写会

写直达与写会

在这里插入图片描述
对于程序中操作的数据,我们不仅要读取,并且还要进行写入操作。不同线程之间很多时候操作的是Cache保留的内存副本。那么我们在执行写入操作时是写入到Cache中还是内存中呢,上图展示了两种写入数据的策略:

  • 1、写直达;写入数据无论是否命中Cache,最终都会同步到主内存中去,这种和volatile的方式比较相像。
  • 2、写回;我们在Cache Line中加入了标记位 “脏”,“脏”表示当前Cache中的数据和主内存中不一致。在进行数据写入时,如果缓存命中,会直接写入到Cache line中同时标记为“脏”。如果缓存不命中,会通过直接映射的方式查找数据在内存中的地址对应的Cache line,如果对应的Cache line为“脏”,会将数据写回到主内存中,否则会将内存中地址对应的数据加载到Cache line,然后将要写入的数据 写入到Cache line同时标记为“脏”;

缓存一致性

上面我们讲到,计算机为了考虑性能,当我们更新数据时,会优先访问Cache,如果没有命中的情况下会访问内存。上一篇博文性能与优化时我们讲到提升性能可以从“提升主频”和“增加并行计算提升吞吐率”方面来入手,在提升主频非常首先的情况下,当前我们计算机从多核的角度出发提升性能。此时如果更新数据还是优先Cache的策略时就会出现缓存不一致的现象,下面举一个实际的案例来理解;
在这里插入图片描述
这里我们使用一个双核的CPU来作演示,主内中存储着IPhone手机的价格8000RMB,过了一段时间后IPhone手机降价到6000RMB,此时我们将更新后的价格通过CPU核心1写入,这里我们采用“写回”策略,缓存命中存储到Cache line中同时标记位“脏”,等到下次数据交换时才会将写入到主内存中。对于单核的CPU来说,这种方式是没有问题的。但是多核的情况下,就会出现问题,如果此时CPU核2访问IPhone的价格,就会读取到之间缓存的价格8000RMB。

缓存一致性需要解决的问题

上面我们我们说到多核CPU存在的缓存不一致的问题,解决这个问题必须要做到以下两点才是合理的:

  • 1、写广播,我们在一个CPU核中的数据更新Cache操作,必须传播到其他CPU核的Cache line中。
  • 2、事务的串行化,是说我们在一个CPU核中的更新数据操作,在其他CPU核中顺序必须是相同的。

对于第一点“写广播”我们比较容易理解,如何理解事务的串行化呢?这里还是使用IPhone手机降价的案例,不过这次我们用四核的CPU进行数据的更新;
在这里插入图片描述
CPU核心1更新IPhone手机价格为6000RMB,几乎同一时间CPU核心2更新IPhone手机价格为5000RMB。这两个数据更新操作传播到CPU核心3和CPU核心4,但是两者的更新顺序不一致,核心3是先更新为5000RMB后更新为6000RMB,核心4是先更新为6000RMB后更新为5000RMB。最后的结果是CPU核心3中的价格是6000RMB,CPU核心4中的价格是5000RMB。
这里为了保证事务的串行化我们使用的是“锁”的概念,只有拥有对应Cache line锁的CPU核才可以进行数据更新操作;

MESI协议实现缓存一致性

MESI是一种基于写失效的协议,同一时间只有一个CPU核来更新数据,其他CPU核来同步数据。写入数据的CPU和会广播一个“失效”请求,其他CPU核收到后会查看本地是否有相应的Cache line,如果有则标记位“失效”;
对于的也有写广播的协议,与“写失效”协议相比,不仅接受广播请求,同时还会更新对应的Cache line数据;这样会占用更多的总线带宽。

MESI 协议的由来呢,来自于我们对 Cache Line 的四个不同的标记,分别是:

  • M:代表已修改(Modified)
  • E:代表独占(Exclusive)
  • S:代表共享(Shared)
  • I:代表已失效(Invalidated)

这里我们给出四种状态的有限状态机:
在这里插入图片描述
参考连接:
MESI协议:如何让多核CPU的高速缓存保持一致?
维基百科

博文中出现的代码,详情点击github地址

创作不易,喜欢的话记得关注和收藏哦!持续更新…

猜你喜欢

转载自blog.csdn.net/weixin_42662358/article/details/107118977