likes
comments
collection
share

CPU中的MESI缓存最终一致性---CPU为什么需要缓存

作者站长头像
站长
· 阅读数 15

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」

前言

  • 锁章节我们已经发布了【java对象在内存中如何分布】、【java有哪些锁】、【synchronized和volatile】。在上文中分析volatile指令重排序的时候看到一片文章介绍CPU缓存一致性的问题。
  • 因为volatile的禁止指令重排序是因为实现了内存屏障。另外一个特性是内存可见性他的实现是通过CPU 的MESI 来实现的。
  • 当A线程将数据修改后刷回主内存中,CPU并同时告知其它线程内的对应数据失效,需要从新获取了

什么是MESI

CPU中的MESI缓存最终一致性---CPU为什么需要缓存

  • MESI其实就是四个单词的缩写,他们是描述线程内数据副本的一个状态。我们通过MESI来看看之前volatile实现内存可见性的流程

CPU中的MESI缓存最终一致性---CPU为什么需要缓存

  • CPU缓存数据并不是缓存需要的数据而是按块为基本单位的,这里是64KB为最小单元。所以我们修改了1个地方的值附近的其他的值也都会失效,进而其他线程会同步数据,这就是伪共享。其实这里我们的mysql设计也是如此,在我们便利查询时数据也是按页进行最小划分的,页的大小时16KB。

CPU为什么需要缓存

  • 计算机内部存储数据也是按块进行存储的,这样的存储方式导致我们不能人性的取,任意存取就会造成我们交互次数的增加。虽然CPU很快,但是内存的速度跟不上CPU的速度,所以将数据打包方式存取是最可取的。
  • 在我们网络开发中读取字节也是同样的道理。我们每次正常情况下都是读取1024字节,这样减少我们的网络交互

CPU有了MESI,为什么Java还要volatile

  • 首先Java虚拟机为了提高运行效率会出现指令重排,这也是volatile特性之一
  • CPU的MESI保证的是单个CPU的单个位置的可见。但是volatile是全部CPU的操作。所以volatile还是很有必要的

在一个典型系统中,可能会有几个缓存(在多核系统中,每个核心都会有自己的缓存)共享主存总线,每个相应的CPU会发出读写请求,而缓存的目的是为了减少CPU读写共享主存的次数。

  • 一个缓存除在Invalid状态外都可以满足cpu的读请求,一个Invalid的缓存行必须从主存中读取(变成S或者 E状态)来满足该CPU的读请求。
  • 一个写请求只有在该缓存行是M或者E状态时才能被执行,如果缓存行处于S状态,必须先将其它缓存中该缓存行变成Invalid状态(也既是不允许不同CPU同时修改同一缓存行,即使修改该缓存行中不同位置的数据也不允许)。该操作经常作用广播的方式来完成,例如:RequestFor Ownership (RFO)。
  • 缓存可以随时将一个非M状态的缓存行作废,或者变成Invalid状态,而一个M状态的缓存行必须先被写回主存。
  • 一个处于M状态的缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S状态之前被延迟执行。
  • 一个处于S状态的缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
  • 一个处于E状态的缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S状态。
  • 对于ME状态而言总是精确的,他们在和该缓存行的真正状态是一致的。而S状态可能是非一致的,如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经
  • 独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。
  • 从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成Invalid状态,而修改E状态的缓存不需要使用总线事务。

案列

  • 上面有介绍到CPU缓存数据单元是64K 。 加入我们Java多线程操纵的两个变量在同一块中,那么一个线程修改了a变量,另外一个线程操作的b变量也涉及到数据同步问题。这里我们可以看下马士兵大牛提供的一份代码,我在本地运行了一下,感觉挺好玩的。
 ​
 @Data
 class Store{
     private volatile long p1,p2,p3,p4,p5,p6,p7;
     private volatile long p;
     private volatile long p8,p9,p10,p11,p12,p13,p14;
 }
 ​
 public class StoreRW {
     public static Store[] arr = new Store[2];
     public static long COUNT = 1_0000_0000l;
     static {
         arr[0] = new Store();
         arr[1] = new Store();
     }
 ​
     public static void main(String[] args) throws InterruptedException {
         Store store = new Store();
         final Thread t1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 for (long i = 0; i < COUNT; i++) {
                     arr[0].setP(i);
                 }
             }
         });
         final Thread t2 = new Thread(new Runnable() {
             @Override
             public void run() {
                 for (long i = 0; i < COUNT; i++) {
                     arr[1].setP(i);
                 }
             }
         });
         final long start = System.currentTimeMillis();
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         final long end = System.currentTimeMillis();
         System.out.println(end - start);
     }
 }
  • 代码很简单,就是两个线程不断的操作两个变量。如果我们将对象中的多余的属性去除。像这样Store中只保留p一个属性
 ​
 @Data
 class Store{
     private volatile long p;
 }
  • 运行我们的程序发现基本上稳定在100毫秒。如果我加上哪些无关的14个long类型的属性。那么程序能稳定在70毫秒。这里程序运行时间取决取电脑的配置。但是不管配置如何肯定能看到加上和不加上14个变量的区别。
  • 这里就涉及到CPU缓存单元。如果只有一个属性。那么a r r数组中的两个对象极有肯能在同一个缓存块中。那么线程A操作a对象,那么线程B就会出现一次同步。但是加上14个变量能够保证a r r数组的两个对象肯定不在同一个单元块中
  • 因为加上14个变量后,一个Store就占15*8=120个字节。那么不管怎么放两个Store肯定不会在同一个块中。而且p变量还是在中间的。所以才会出现这种效果。
  • 针对这种操作有的人会认为代码没有美感,但是这样的确能够提高性能。JDK针对次也提供了注解@sun.misc.Contended ; 不过我测试了一下感觉性能提升没有14个变量大。马老师

总结

  • 今天的介绍就到这里吧。主要在与MESI的理解。

参考文章

CPU缓存一致性案列分析详细