一些计算机组成和缓存的概念
计算机的组成
存储器的层次结构
超线程
-
CPU由ALU(计算单元)、PC(指令计数器,存放下一条指令地址)、Register(寄存器,存放指令执行所需的数据)、Cache(L1级缓存,L2级缓存,L3级缓存)组成
-
L1级缓存和L2级缓存是一个核里的,L3级缓存是多核共享的
-
线程之间的切换,是CPU把当前线程的现场(指令和数据)保存到内存,下一次再从内存把线程现场读取到CPU中(PC和Register)执行
-
线程切换读写内存特别耗时间,为了解决这个问题,在一个CPU核内,保存两套PC和Register,那么ALU就可以在这两套间进行切换执行,比保存到内存中高效多了,这种就叫超线程
我们经常能听到的 8核16线程指的就是超线程,一个核内有两套PC和Register
Cache Line 的概念 缓存行对齐 伪共享
CPU一次性从内存读取的数据长度称为字长(intel的cpu 一般是64 byte),读取后保存在CPU的缓存里叫Cache Line
当两个CPU 核读取了同一块内存,当一个核改变了某个变量,要同步到另一个核的Cache Line中,叫做一致性协议
Java的volatile 关键字就是保证变量的一致性
Cache Line越大,局部性空间效率越高,但读取时间慢
Cache Line越小,局部性空间效率越低,但读取时间快
去一个折中值,目前多用64 byte
MESI Cache 一致性协议
Cache Line 有4中状态
- Modified :被修改了
- Exclusive :独享
- Shared :分享,只读不改
- Invalid :失效
当一个缓存行修改了数据,状态为Modified,这时候这个核会通知(使用缓存锁进行通知)使用同一个缓存行的其它核你们的数据Invalid了,需要去内存重新读取。
由volatile修饰的数据,在被某个核读取到Cache Line中修改后,会把数据重新推送到内存,这样别的核读内存的时候,数据已经是被修改了的
缓存锁实现之一:有些无法被缓存的数据,或者跨越多个缓存行的数据依然必须使用总线锁
因此当两个volatile隔的比较近在同一个缓存行里的时候,执行效率就会低了,因为需要不断的通知别的CPU去内存读新的数据
disruptor(单机版队列中速度最快的)中就是利用到了Cache Line 一致性问题来提高性能
public long p1,p2,p3,p4,p5,p6,p7; //cache line padding,一个long 数据占8byte,因此和真实数据组合成64 byte,正好是一个Cache Line 长度
private volatile long cursor = INITIAL_CURSOR_VALUE; //真实数据
public long p8,p9,p10,p11,p12,p13,p14; //cache line padding
以上这样写了后,cursor就不会和其他真实数据处于同一Cache Line,因此就避免了CPU频繁读写内存数据
volatile 保证变量的一致性 和 static 和 synchronized
变量放在主存区上,使用该变量的每个线程,都将从主存区拷贝一份到自己的工作区上进行操作。
volatile, 声明这个字段易变(可能被多个线程使用),Java内存模型负责各个线程的工作区与主存区的该字段的值保持同步,即一致性。
static, 声明这个字段是静态的(可能被多个实例共享),在主存区上该类的所有实例的该字段为同一个变量,即唯一性。
volatile, 声明变量值的一致性;static,声明变量的唯一性。
此外,volatile同步机制不同于synchronized, 前者是内存同步,后者不仅包含内存同步(一致性),且保证线程互斥(互斥性)。
static 只是声明变量在主存上的唯一性,不能保证工作区与主存区变量值的一致性;除非变量的值是不可变的,即再加上final的修饰符,否则static声明的变量,不是线程安全的。
MESI 只是volatile的一种实现方式
volatile 可以解决CPU的乱序执行
CPU的读等待同时,其他指令执行
读指令的同时可以同时执行不影响的其他指令
而写的同时可以进行合并写WCBuffer
这样CPU的执行就是乱序的
必须使用Memory Barrier来做好指令排序
volatile的底层就是这么实现的(Windows是lock指令)
volatile 解决cpu乱序执行的典型案例是 单例模型中双重检查实现方案
WCBuffer 是在L1缓存上面或者前面的一个缓存,总共4个字节