计算机中,所有的运算操作都是由CPU的寄存器来完成的,CPU指令的执行过程需要涉及数据的读取和写入操作,CPU所能访问的所有数据只能是计算机的主存(通常RAM)。
CPU和主存两边的速度严重的不对等,通过传统FSB(Front Side Bus)直接内存的访问方式很明显会导致CPU资源受到大量的限制,降低CPU整体的吞吐量,于是才有了在CPU和主内存之间增加缓存的设计,缓存有一级缓存(一级缓存又分为一级指令缓存和一级数据缓存)、二级缓存、三级缓存,主存到CPU之间的缓存的访问速度是越来越快的,这种方式极大的提高了CPU的吞吐能力,缓存模型如下:
CPU Cache中的最小缓存单位是Cache Line,CPU缓存由许多个Cache Line构成,一个缓存行64个字节。由于缓存的出现,虽然提高了CPU的吞吐能力,但是同时也引入了缓存不一致的问题。比如在多线程时i++就可能会产生缓存不一致性的问题。主流的解决方案是:
1、锁总线
2、通过缓存一致性协议
锁总线是一种悲观的实现方式,CPU和其他组建的通信都是通过总线(数据总线、控制总线、地址总线)来进行,如果采用总线加锁的方式,则会阻塞其他CPU对其他组建的访问,从而使得只有一个CPU能够访问这个变量的内存,这种方式效率低下,一般都是通过第二种方式来解决不一致的问题。
缓存一致性协议中的MESI协议保证了每个缓存中使用的共享变量副本都是一致的。当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU Cache中也存在一个副本,那么进行如下操作:
1、读取操作,不做任何处理,只是将Cache中的数据读取到寄存器。
2、写入操作,发出信号通知其他CPU将该变量的Cache Line置为无效状态,其他CPU在进行该变量读取的时候不得不到主内存中再次获取。
这里举个栗子:如果x,y变量在一个缓存行里,开启两个线程对x和y变量进行修改,那么一个线程修改x/y变量时得通知另一个线程修改过了x/y变量,这样会影响它的性能。
如果我们把x和y放在不同的缓存行里,那么它们之间就不会相互影响,这个也叫缓存行对齐。
补充知识:
一个java对象在内存中的存储布局:对象头markword(8个字节)、类型指针class pointer(8个字节,经过压缩后是4个字节)、实例数据instance data(看实际的数据大小,比如一个long类型对象占8个字节),对齐padding(这块是看前面的数据量能否被8个字节整除,不能才去补齐,比如markword—8字节+class pointer—8字节+instance data—long类型+int类型—8+4字节,那么padding就是4字节)。
那么x所在的对象里加上一些冗余的数据使得该对象为64字节,那么x和y就不在一个缓存行了。实际应用中,如disruptor就采用了缓存行对齐伪共享的技术提升了性能。