在阅读VIVO的博客高性能无锁队列 Disruptor 核心原理分析及其在i主题业务中的应用的时候,了解到 Disruptor
高性能的核心原因:
- 空间预分配
- 避免伪共享
- 无锁
其中 伪共享 的概念之前没有了解过,故特地了解学习了下,主要涉及到一些基础的概念:
- CPU的分级缓存机制
- volatile的内存可见性
- Java long类型/Long类型的字节大小
众所周知,CPU在读取内存中的数据时,并不是读取的直接内存,而是从L1/L2/L3缓存中读取数据,而读取缓存也并非按1字节
的读取,而是按照缓存行
(通常64字节)一块一块的读取,以此来提高读取效率。
周所也周知,现代计算机都是多核CPU在运行,线程都会被分配CPU来执行,所以线程内的变量数据是需要读取到CPU Cache中才能够对CPU可见的,为了解决内存在CPU1中修改后CPU2不可见(脏读)的问题,在Java中有设计变量修饰符volatile
来修饰变量,以此来实现数据在多个CPU之间不会产生脏读问题,内存在任何一个CPU上发生修改后,在其他CPU上均不可用而丢弃重新从内存中获取。
当然,也正是因为以上设计,带来了一些预期外的结果(不是问题或bug),如下代码所示:
对于下面的代码:
public class FalseSharing {
public static void main(String[] args) throws InterruptedException {
int num = 100000000;
Pointer pointer1 = new Pointer();
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for(int i = 0; i < num; i++){
pointer1.x++;
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < num; i++){
pointer1.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("pointer1=" + (System.currentTimeMillis() - start));
}
}
class Pointer {
volatile long x;
volatile long y;
}
上述代码在我的电脑上执行,需要3.5秒+
,多次执行亦然如此,而我们只需稍作调整,改成如下代码,则只需要0.5秒左右
即可执行完毕,这是为何?
public class FalseSharing {
public static void main(String[] args) throws InterruptedException {
int num = 100000000;
Pointer2 pointer2 = new Pointer2();
long start2 = System.currentTimeMillis();
Thread t3 = new Thread(() -> {
for(int i = 0; i < num; i++){
pointer2.x++;
}
});
Thread t4 = new Thread(() -> {
for(int i = 0; i < num; i++){
pointer2.y++;
}
});
t3.start();
t4.start();
t3.join();
t4.join();
System.out.println("pointer2=" + (System.currentTimeMillis() - start2));
}
}
class Pointer2 {
volatile long x;
// long p1, p2, p3, p4, p5, p6, p7;
Long z1, z2; long z3;
volatile long y;
}
以上现象对应的机制,正是因为缓存行的设计。
在上面的代码中,包含了线程t1/t2(t3和t4等同)两组线程,分别对x和y进行累加操作,看似是两个线程(CPU在分别对两块内存中的变量执行累加),但是因为CPU是以缓存行的方式读取内存,Pointer1
中的x和y在内存中时相邻的两块内存,Java的基本数据类型long类型,占用8字节
,所以x和y(加起来16字节)被放到同一个缓存行了,当CPU1对x做了修改后,CPU2读取到y时发现,对应缓存行已经失效了,所以不得不重新从内存中重新读取数据,从而导致了效率降低。
而我们下面给出的代码,Pointer2
的x和y变量间,被塞了两组变量:
- long p1, p2, p3, p4, p5, p6, p7; --long类型,占用8个字节, 8*7=56
- Long z1, z2; long z3; --包装Long类型,根据计算机不同有所不同,在我电脑上占用24字节, 24+24+8=56
两组变量任何一组都可以解决伪共享的问题,之所以塞两组变量,都是为了验证缓存行的存在和解决方案。
方案一直接使用7个基本数据类型
,占用了56个字节,加上x变量自身,刚好占用一个完整的缓存行64字节,y只能在另一个缓存行了。
方案二使用了2组包装数据类型,1组基本数据类型
,加起来依旧占用的是56个字节,依旧能将一个缓存行占满,解决缓存行失效问题。
通过对这块的学习,对基础知识又做了一个巩固,造火箭的知识又增加了。🤡
博文参考:
实操代码:FalseSharing