一、CPU Cache
存储设备往往是速度越快价格越低廉,速度越快价格越低廉。
在计算机中,CPU 的速度远高于主存的速度,而主存的速度又远高于磁盘的速度。为了解决不同存储部件的速度不对等问题,让高速设施充分发挥性能,引入了多级缓存机制。
为了解决内存和 CPU 的速度不匹配问题,相继引入了 L1 Cache、L2 Cache、L3 Cache,数字越小,容量越小,速度越快,地位越靠近 CPU。
当初的 CPU 都是由多个处理器,每个处理器由多个外围形成。一个处理器对应一个物理插槽,不同的处理器间通过 QPI 总线相连。一个处理器间的多核共享 L3 Cache。一个核蕴含寄存器、L1 Cache、L2 Cache,下图是 Intel Sandy Bridge CPU 架构:
二、缓存行与伪共享
缓存中的数据并不是独立的进行存储的,它的最小存储单位是缓存行,缓存行的大小是 2 的整数幂个字节,最常见的缓存行大小是 64 字节。CPU 为了执行的高效,会在读取某个对象时,从内存上加载 64 的整数倍的长度,来补齐缓存行。
以 Java 的 long 类型为例,它是 8 个字节,假如咱们存在一个长度为 8 的 long 数组 arr,那么 CPU 在读取 arr[0] 时,首先查问缓存,缓存没有命中,缓存就会去内存中加载。因为缓存的最小存储单位是缓存行,64 字节,且数组的内存地址是间断的,则将 arr[0] 到 arr[7] 加载到缓存中。后续 CPU 查问 arr[6] 时候也能够间接命中缓存。
当初假如多线程状况下,线程 A 的执行者 CPU Core-1 读取 arr[1],首先查问缓存,缓存没有命中,缓存就会去内存中加载。
从内存中读取 arr[1] 起的间断的 64 个字节地址到缓存中,组成缓存行。因为从 arr[1] 起,arr 的长度不足够 64 个字节,只够 56 个字节。假如最初 8 个字节内存地址上存储的是对象 bar,那么对象 bar 也会被一起加载到缓存行中。
当初有另一个线程 B,线程 B 的执行者 CPU Core-2 去读取对象 bar,首先查问缓存,发现命中了,因为 Core-1 在读取 arr 数组的时候也顺带着把 bar 加载到了缓存中。
这就是缓存行共享,听起来不错,然而一旦牵扯到了写入操作就不妙了。
假如 Core-1 想要更新 arr[7] 的值,依据 CPU 的 MESI 协定,那么它所属的缓存行就会被标记为生效。因为它须要通知其余的 Core,这个 arr[7] 的值曾经被更新了,缓存曾经不再精确了,你必须得从新去内存拉取。然而因为缓存的最小单元是缓存行,因而只能把 arr[7] 所在的一整行给标识为生效。
此时 Core-2 就会很郁闷了,刚刚还可能从缓存中读取到对象 bar,当初再读取却被告知缓存行生效,必须得去内存从新拉取,延缓了 Core-2 的执行效率。
这就是缓存伪共享问题,两个毫无关联的线程执行,一个线程却因为另一个线程的操作,导致缓存生效。这两个线程其实就是对同一缓存行产生了竞争,升高了并发性。
三、Disruptor 缓存行填充
Disruptor 为了解决伪共享问题,应用的办法是缓存行填充。这是一种以空间换工夫的策略,次要思维就是通过往对象中填充无意义的变量,来保障整个对象独占缓存行。
举个例子,以 Disruptor 中的 Sequence 为例,在 volatile long value 的前后各搁置了 7 个 long 型变量,确保 value 独占一个缓存行。
public class Sequence extends RhsPadding {
private static final long VALUE_OFFSET;
static {VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));
...
}
...
}
class RhsPadding extends Value {protected long p9, p10, p11, p12, p13, p14, p15;}
class Value extends LhsPadding {protected volatile long value;}
class LhsPadding {protected long p1, p2, p3, p4, p5, p6, p7;}
如下图所示,其中 V 就是 Value 类的 value,P 为 value 前后填充的无意义 long 型变量,U 为其它无关的变量。不论什么状况下,都能保障 V 不和其余无关的变量处于同一缓存行中,这样 V 就不会被其余无关的变量所影响。
这里的 V 也不限定为 long 类型,其实只有对象的大小大于等于 8 个字节,通过前后各填充 7 个 long 型变量,就肯定可能保障独占缓存行。
此处以 Disruptor 的 RingBuffer 为例,最右边的 7 个 long 型变量被定义在顶级父类 RingBufferPad 中,最左边的 7 个 long 型变量被定义在 RingBuffer 的最初一行变量定义中,这样所有的须要独占的变量都被左右 long 型给突围,确保会独占缓存行。
public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {
public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;
protected long p1, p2, p3, p4, p5, p6, p7;
...
}
abstract class RingBufferFields<E> extends RingBufferPad
{...}
abstract class RingBufferPad {protected long p1, p2, p3, p4, p5, p6, p7;}
四、@Contended
在 JDK 1.8 中,提供了 @sun.misc.Contended 注解,应用该注解就能够让变量独占缓存行,不再须要手动填充了。另外,关注公众号 Java 技术栈,在后盾回复:Java,能够获取我整顿的 Java 1.8 系列教程,十分齐全。
留神,JVM 须要增加参数 -XX:-RestrictContended 能力开启此性能。
如果该注解被定义在了类上,示意该类的每个变量都会独占缓存行;如果被定义在了变量上,通过指定 groupName,雷同的 groupName 会独占同一缓存行。
// 类前加上代表整个类的每个变量都会在独自的 cache line 中
@sun.misc.Contended
public class ContendedData {
int value;
long modifyTime;
boolean flag;
long createTime;
char key;
}
// 同一 groupName 在同一缓存行
public class ContendedGroupData {@sun.misc.Contended("group1")
int value;
@sun.misc.Contended("group1")
long modifyTime;
@sun.misc.Contended("group2")
boolean flag;
@sun.misc.Contended("group3")
long createTime;
@sun.misc.Contended("group3")
char key;
}
@Contended 在 JDK 源码中曾经有所利用,以 Thread 类为例,为了保障多线程状况下随机数的操作不会产生伪共享,相干的变量被设置为同一 groupName。
public class Thread implements Runnable {
...
// The following three initially uninitialized fields are exclusively
// managed by class java.util.concurrent.ThreadLocalRandom. These
// fields are used to build the high-performance PRNGs in the
// concurrent code, and we can not risk accidental false sharing.
// Hence, the fields are isolated with @Contended.
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
...
}
五、速度测试
将 volatile long value 封装为对象,四线程并行,每个线程循环 1 亿次,对 value 进行更新操作,测试缓存行对速度的影响。
- CPU:AMD 3600 3.6 GHz
- Memory:16 GB
作者: Jitwxs\
链接: https://jitwxs.cn/13836b16.html
近期热文举荐:
1.Java 15 正式公布,14 个新个性,刷新你的认知!!
2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!
3. 我用 Java 8 写了一段逻辑,共事直呼看不懂,你试试看。。
4. 吊打 Tomcat,Undertow 性能很炸!!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!