并发编程无声的性能杀手
数据在内存中以 page 为单位存在;在cache 里以 cache line 为单位
伪共享:缓存系统以 cache line为单位存储,多线程修改互相独立的变量时,如果这些变量共享同一个cache line,就会无意中影响彼此的性能
CPU 取数逻辑
CPU 执行运算的时,经 L1 > L2 > L3 > 主存(物理内存)
缓存未命中的消耗
cpu到 | 周期约(cycle) | 耗时约(ns) |
---|---|---|
主存 | 60-80 | |
QPI | 20 | |
L3 | 40-45 | 15 |
L2 | 10 | 3 |
L1 | 3-4 | 1 |
寄存器 | 1 |
Cache Line
cacheLine 最常见 64 字节(2 的整数幂,32-256)
java long 8 字节,一个 cache line 可放 8 个 long 变量
访问 long[0] ,0-7 元素都被会加载到 cache 里,即便此后并不需要访问 1-7 的元素
好处:
恰好访问完 0 元素就像访问 1 的话(遍历数组),赚到
副作用:
整体加载,同一 cache line 上的数据是绑在一根绳上的蚂蚱
如果数据结构中的项在内存中不是彼此相邻的(如链表),将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中
False Sharing
多个线程操作不同的成员变量,但是相同的缓存行
volatile long head;
volatile long tail;
head 和 tail 会被加载到一个 cache line 中,但多线程的情况
head 被 Thread A 写入,tail 被 Thread B 读取。A 和 B 运行于不同 CPU 核心上
假设 head 被 A 更新了,cache 和主存中 head 的值都会被更改。导致所有 head 所在的 cache line 都失效,包括 B 使用的 cache line——即便它其实并不关心 head 的值
而 B 此时如果需要读取 tail,就必须重新从主存中加载 head 和 tail 到一个 cache line,即便 tail 没有被更改过
甚的情况是如果 B 也是写入操作,那么 A 和 B 任意一方的写操作都会让对方的 cache line 整体失效从而导致重新从主存加载
问题在于这一切都在幕后悄然发生,没有任何编译告警提示你说你这段代码并发性能会非常低下
core1 线程想要更新 X ,同时 core2 线程想要更新 Y 。但这两个频繁改动的变量都处于同一条缓存行
两个线程会轮番发送 RFO 消息,占得此缓存行的拥有权
core1 取得拥有权开始更新 X,则 core2 对应的缓存行要设为 I 状态
core2 取得拥有权开始更新 Y,则 core1 对应的缓存行要设为 I 状态 (失效态)
轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据
更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享
public class FalseShareTest implements Runnable {
public static int NUM_THREADS = 4;
public final static long ITERATIONS = 500L * 100L * 100L;
private final int arrayIndex;
private static VolatileLong[] longs;
public static long SUM_TIME = 0L;
public FalseShareTest(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
for (int j = 0; j < 10; j++) {
System.out.println(j);
if (args.length == 1) {
NUM_THREADS = Integer.parseInt(args[0]);
}
longs = new VolatileLong[NUM_THREADS];
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
final long start = System.nanoTime();
runTest();
final long end = System.nanoTime();
SUM_TIME += end - start;
}
System.out.println("平均耗时:" + SUM_TIME / 10);
}
private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseShareTest(i));
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
}
@Override
public void run() {
long i = ITERATIONS + 1;
while (0 != --i) {
longs[arrayIndex].value = i;
}
}
public final static class VolatileLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; //屏蔽此行
}
}