云时代架构阅读笔记九——Disruptor无锁框架为啥这么快
1.1 CPU缓存
在现代计算机当中,CPU是大脑,最终都是由它来执行所有的运算。而内存(RAM)则是血液,存放着运行的数据;但是,由于CPU和内存之间的工作频率不同,CPU如果直接去访问内存的话,系统性能将会受到很大的影响,所以在CPU和内存之间加入了三级缓存,分别是L1、L2、L3。
当CPU执行运算时,它首先会去L1缓存中查找数据,找到则返回;如果L1中不存在,则去L2中查找,找到即返回;如果L2中不存在,则去L3中查找,查到即返回。如果三级缓存中都不存在,最终会去内存中查找。对于CPU来说,走得越远,就越消耗时间,拖累性能。
在三级缓存中,越靠近CPU的缓存,速度越快,容量也越小,所以L1缓存是最快的,当然制作的成本也是最高的,其次是L2、L3。
CPU频率,就是CPU运算时的工作的频率(1秒内发生的同步脉冲数)的简称,单位是Hz。主频由过去MHZ发展到了当前的GHZ(1GHZ=10^3MHZ=10^6KHZ= 10^9HZ)。
内存频率和CPU频率一样,习惯上被用来表示内存的速度,内存频率是以MHz(兆赫)为单位来计量的。目前较为主流的内存频率1066MHz、1333MHz、1600MHz的DDR3内存,2133MHz、2400MHz、2666MHz、2800MHz、3000MHz、3200MHz的DDR4内存。
1.2 缓存行
当数据被加载到三级缓存中,它是以缓存行的形式存在的,不是一个单独的项,也不是单独的指针。
在CPU缓存中,数据是以缓存行(cache line)为单位进行存储的,每个缓存行的大小一般为32—256个字节,常用CPU中缓存行的大小是64字节;CPU每次从内存中读取数据的时候,会将相邻的数据也一并读取到缓存中,填充整个缓存行;
可想而知,当我们遍历数组的时候,CPU遍历第一个元素时,与之相邻的元素也会被加载到了缓存中,对于后续的遍历来说,CPU在缓存中找到了对应的数据,不需要再去内存中查找,效率得到了巨大的提升;
1.3 CAS
前2节,我们已经讲了缓存行、伪共享的知识,本节来阐述Disruptor中另一个知识点—-CAS;那么,CAS是什么呢?
在Java中,多线程之间如何保证数据的一致性?想必大部分都会异口同声地说出锁—-synchronized锁。在JDK1.5之前,的确是使用synchronized锁来保证数据的一致性。但是,synchronized锁是一种比较重的锁,俗称悲观锁。在较多线程的竞争下,加锁、释放锁会对系统性能产生很大的影响,而且一个线程持有锁,会导致其他线程的挂起,直至锁的释放。
那么,有没有比较轻的锁呢,答案是有的!与之相对应的是乐观锁!乐观锁虽然名称中带有锁,但实际在代码中是不加锁的,乐观锁大多实现体现在数据库sql层面,通常是的做法是:为数据增加一个版本标识,在表中增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
update XXX_TABLE SET MONEY = 100 AND VERSION = 11 WHERE ID = 1 AND VERSION = 10;
这就是乐观锁!
上面说到了数据库层面的乐观锁,那么代码层面有没有类似的实现?答案是,有的!那就是我们本小节的主角—CAS;
CAS是一个CPU级别的指令,翻译为Compare And Swap比较并交换;
CAS是对内存中共享数据操作的一种指令,该指令就是用乐观锁实现的方式,对共享数据做原子的读写操作。原子本意是“不能被进一步分割的最小粒子”,而原子操作意为”不可被中断的一个或一系列操作”。原子变量能够保证原子性的操作,意思是某个任务在执行过程中,要么全部成功,要么全部失败回滚,恢复到执行之前的初态,不存在初态和成功之间的中间状态。
CAS有3个操作数,内存中的值V,预期内存中的值A,要修改成的值B。当内存值V和预期值相同时,就将内存值V修改为B,否则什么都不做。