1. 为什么会有伪共享问题
为什么会有伪共享问题就要从CPU多级缓存说起,计算机中每个CPU Core都有自己的多级缓存,CPU操作数据的时候先从自己的Cache中查找数据,如果没有找到,再从内存中读取,然后将读取的数据放到自己的Cache中。
Cache Line是CPU从内存读取数据到自己的Cache的单位,一般Linux系统中Cache Line的大小是64字节。也就意味着CPU一次载入的数据大小是64字节。
CPU在取数据到自己的Cache中,除了取目标数据还会取该数据相邻的数据到Cache中,主要是依据时间局部性和空间局部性。
多级缓存又会导致缓存一致性问题,为了解决这个问题,又提出了缓存一致性协议MESI。MESI是为了解决缓存一致性的问题,但是也引入了另外一个问题就是伪共享问题。
关于MESI可参考:JVM:Java内存模型(1)
2. 伪共享产生的流程
假设现在有两个CPU Core,分别是Core1和Core2,这两个Core在运算过程中,Core1需要变量x,Core2需要变量y。x和y都是long类型的变量,在内存中的地址是连续的。
2.1 缓存数据
1.由于CPU每次读取数据都会读取Cache Line大小,也就是64个字节根据空间局部性原理。
2.Core1在缓存x的同时也会缓存y,并且x和y在同一个Cache Line当中。
3.同理Core2在缓存y的时候,也要缓存x,x和y也在同一个Cache Line当中。
4.这个时候两个Cache Line的为Shared状态。
2.2 Core1修改数据
1.现在Core1要修改x变量的值,发现x所在的Cache Line是Shared状态
2.Core1需要先广播,然后Core2收到这个广播之后,把自己的Cache Line设置为Invalid状态。
3.然后Core1修改x的值,然后把Cache Line设置为Modified状态。
2.3 Core2修改数据
1.现在Core2要修改y的值,发现Cache Line是Invalid的状态。
2.然后Core1缓存了相同数据的,并且是Modified,需要先把Core1里面修改了的值同步到内存。
3然后Core2再从内存读取修改后的值读取到自己的缓存中。
4.Core2修改y的值,修改之后把Cache Line标记为Modified的。
5.Core1的Cache Line标记为Invalid。
上述流程,虽然
x只有Core1进行读写操作,y只有Core2,进行读写操作,按理说直接在缓存中操作即可,但是现在需要不停的从缓存和内存交换数据,如果这是一个循环的话,会很浪费性能。核心原因就在于Cache Line中同时缓存了x和y,而x和y被不同的CPU Core使用,导致了缓存失效,这就是伪共享问题。
3. 伪共享的代码示例
- 现在有一个对象,有三个变量,三个线程他同时对这个三个变量进行修改。
public class ObjDemo {
volatile long a;
volatile long b;
volatile long c;
}
- 对这三个变量进行修改,注意
join方法是阻塞主线程,防止主线程提前结束了,同时保证t1,t2,和t3都能完成。
public class Demo {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
ObjDemo objDemo = new ObjDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
objDemo.a++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
objDemo.b++;
}
});
Thread t3 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
objDemo.c++;
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("总耗时:" + (System.currentTimeMillis() - start));
}
}
上述代码总共耗时大概2000ms以上,三个线程,在修改对应long类型的变量的时候,需要把变量缓存到自己的Cache Line中,由于Cache Line一次缓存64个字节,而一个long类型的变量是8字节,由于空间局部性,CPU缓存的时候会把相邻的数据拿到自己的缓存中,所以虽然线程t1不使用b和c也会把这两个变量放到自己的缓存中,其它两个一样。导致在修改的时候,会通知另外两个线程,并且修改了之后需要同步到内存当中,虽然这个变量没有其它核使用。
4. 解决方法1
第一种办法就是把这个三个变量分开,让它们不会缓存在同一个Cache Line中,最简单的做法就是把变量后面填满,刚好是一个Cache Line,这样另外的变量只能在下一个Cache Line当中了。
package com.lee.study.basic;
public class ObjDemo {
volatile long a;
long p1, p2, p3, p4, p5, p6, p7;
volatile long b;
long q1, q2, q3, q4, q5, q6, q7;
volatile long c;
}
5. 解决方法2
在Java8中提供了一个注解@Contented,使用了此注解的类或者变量会在前后加128字节,同时需要添加虚拟机参数-XX:-RestrictContended才会生效
import sun.misc.Contended;
public class ObjDemo {
@Contended
volatile long a;
@Contended
volatile long b;
volatile long c;
}
使用上述两种方法可以,把代码运行时间降低到几百毫秒
参考文档: