缓存一致性协议
在网上的很多文章,都把缓存一致性协议叫做MESI,其实不单单指MESI,MESI只是因特尔CPU的一种协议.
MESI指的其实只是4种状态:缓存被修改状态,独占状态,分享状态,失效状态.
其他缓存一致性协议还有MOSI,MSI.缓存锁,总线锁。
比如Java中volitale就是使用的缓存锁,调用lock指令锁住总线.只要一修改,锁住的这部分数据,那么原数据相关的缓存都会自动失效,然后获取到最新修改后的值.
到后期,一般都不用总线锁了,锁总线期间其他CPU没法访问内存,后面已经都优化成了缓存锁 + 总线索的方式,因为有些缓存无法使用缓存锁来锁定。
缓存行
cpu的速度比内存快的多,大概100比1左右.也就是在超线程期间,访问寄存器的速度比访问外界内存的速度快100倍,在这里指的是访问速度.
如何让访问速度变快呢? 最好的方法就是建立缓存,cpu优先去缓存当中读取数据,如果没有再去内存当中读取数据,然后将数据放入缓存当中.
现在一般都是使用三级缓存,这是硬件与性能平衡之后的结果.
性能时间
寄存器 小于1纳秒.
1级缓存 1纳秒左右.
2级缓存 3纳秒左右
3级缓存 5纳秒最优.
主内存 80纳秒.
重点:没有什么架构是加一层解决不了的,比如说加了一层JVM,就实现了跨操作系统的功能.
当我们读取数据的时候,如果单单只读取一个数据,然后再放入内存当中,此时的效率是很慢的,我们如果需要其他数据了就需要循环往复的读取,在这里我们CPU做了一个优化,他会一次性读取一行的数据,从而避免多次读取操作而降低CPU的性能.
这个数据量太大太小都不好,一个找的慢,一个从内存中读取太大也不好,在三级缓存的应用环境下,最佳大小是64个字节.
当我们明白了CPU读取数据是一行一行读取的,以及缓存行一行大小是64个字节之后.
我们需要保证的是,如何让多核之间保证数据的一致性,这里有个协议就叫做缓存一致性协议.
对这个协议在Java代码层面有优化的实例,disruptor,LinkedBlockingQueue源码就有缓存行优化的概念,定义了多个变量,变量大小大于64byte,那么就减少了共享数据互相通知的时间.
比如disruptor单机版最快MQ,他是一个环形缓冲区,是一个首尾相连的数组,只有一个指针,放完一个消息往下挪一格,如果满了那么使用CAS自旋,访问时候查询一下当前数组是否存在数据,自旋等待.
缓存行优化
在代码中我们也可以利用这一特性进行缓存行的优化
或者是在JDK1.8中使用@Contended注解 并且需要在JVM启动中设置-XX:-RestrictContended取消限制,不然会导致无法生效的问题出现,注意在这里我特地去百度查询过这个注解只有在JDK1.8中才能生效,1.7和1.9好像都是不能使用的,也是一个很奇怪的例子。
总线锁
也是CAS底层原理,当CAS对值进行修改的时候会调用c++的代码,使用硬件级别的总线锁死其他的cpu,在这段修改期间内,不允许其他CPU执行操作.
缓存锁
总线锁的升级版本,会通过修改数据的4种状态来选择对应的操作.
系统屏障
编译器和CPU会在不影响结果(这儿主要是根据数据依赖性)的情况下对指令重排序,使性能得到优化,但是实际情况里面有些指令虽然没有前后依赖关系,但是重排序之后影响到输出结果,这时候可以插入一个内存屏障,相当于告诉CPU和编译器限于这个命令的必须先执行,后于这个命令的必须后执行。
内存屏障的另一个作用是强制更新一次不同CPU的缓存,这意味着如果你对一个volatile字段进行写操作,你必须知道:
一旦你完成写入,任何访问这个字段的线程将会得到最新的值;
在你写入之前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
CPU的乱序执行
CPU分为IO密集型和CPU密集型,当CPU的A线程是IO线程,正在等待内存数据的时候,那么在CPU当中会做一个优化,CPU会先让B线程进行执行,最后执行顺序的结果可能是213,但是并不会妨碍最终数据的一致性,但是当多线程的时候,是有可能出现很致命的错误的.
举个最典型的案例,Double Check Lock单例模式,
publicclassSingleton { privatestaticSingletonsingleton; //private volatile static Singleton singleton;privateSingleton() {} publicSingletongetInstance() { if (singleton==null) { synchronized (Singleton.class){ if(singleton==null) { singleton=newSingleton(); } } } returnsingleton; } }
上述代码虽然使用了双重锁检查尽可能的规避掉了效率上的影响,但是仍然可能会返回半初始化的对象,所以要禁止指令重排序,通过使用volatile关键字的第二种功能。
实例化一个对象要分为三个步骤:
分配内存空间
初始化对象
将内存空间的地址赋值给对应的引用
但是在上图中没有禁止指令重排序的执行顺序可能是231,那么就会返回一个残缺的对象,出现致命的错误.
硬件层面原理
lock总线锁命令调用全内存屏障,其中包含了读写屏障,所以既能保证读取最新的值,又能保证禁止指令重排的功能.
syn的硬件层面也和Volitale类似 都是调用的Lock指令,区别是在JVM中,syn是通过C++调用的同步互斥区来实现的.
HOTSPOT对应源码
HOTSPOT源码执行过程
x86CPU lock指令会调用全屏障