CPU每次读取并不是一个字节一个字节的读取,它会一次读取一块内容,这“块”称之为CPU的缓存行(CPU每次访问主存会很慢;CPU的高速缓存L1,L2,L3;L1,L2是每个CPU Core独有的,L3是所有Core共享的。我们可以把缓存行理解为高速缓存的组成最小单元)。最常见的缓存行大小是64byte(每次都会读取64字节大小)。
CPU 个缓存 及 主存
当CPU从主内存中读取一个变量的时候它会把相邻的数据都一次性的加载到缓存中。当访问相邻数据的时候就不需要再到主存中读取了,直接从缓存中获取,提高执行效率。
private static class CacheLineData {
private volatile long d1 = 0L;
private volatile long d2 = 0L;
private volatile long d3 = 0L;
private volatile long d4 = 0L;
}
在多核core环境下,当有多个线程访问如上变量d1,d2,d3,d4,每个线程都运行在不同的core中
private static CacheLineData cacheLine = new CacheLineData() ;
private static int n = 1000000 ;
public static void main(String[] args) throws Exception {
int loop = 4 ;
Thread[] ts = new Thread[loop] ;
ts[0] = new Thread(() -> {
for (int m = 0; m < n; m++) {
cacheLine.d1 = m ;
}
}) ;
ts[1] = new Thread(() -> {
for (int m = 0; m < n; m++) {
cacheLine.d2 = m ;
}
}) ;
ts[2] = new Thread(() -> {
for (int m = 0; m < n; m++) {
cacheLine.d3 = m ;
}
}) ;
ts[3] = new Thread(() -> {
for (int m = 0; m < n; m++) {
cacheLine.d4 = m ;
}
}) ;
long start = System.currentTimeMillis() ;
for (int i = 0; i < loop; i++) {
ts[i].start() ;
}
for (int i = 0; i < loop; i++) {
ts[i].join() ;
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + " ms") ;
}
如上代码有4个线程分别访问d1,d2,d3,d4。如何产生伪共享的呢?当线程1修改了d1变量后,其它线程的缓存行多会作废并重新从主存中获取数据(volatile 修饰的变量,在多线程情况下访问时当有一个线程修改了这个变量,那么会通过消息总线通知其他线程该变量已经修改并将其置为invalid状态,再使用时必须重新从主存中获取)。
如上示例代码运行结果:
50ms左右
当把volatile修饰符去掉后的运行结果:
private static class CacheLineData {
private long d1 = 0L;
private long d2 = 0L;
private long d3 = 0L;
private long d4 = 0L;
}
7ms左右
方法1、接下来我们可以通过填充的方法来使得每个变量都处在不同的缓存行中。一个long占8个字节(我们这里按照缓存行64个字节来算)
private static class CacheLineData {
private volatile long k0, k1, k2, k3, k4, k5, k6, k7 ;
private volatile long d1 = 0L;
private volatile long n0, n1, n2, n3, n4, n5, n6, n7 ;
private volatile long d2 = 0L;
private volatile long z0, z1, z2, z3, z4, z5, z6, z7 ;
private volatile long d3 = 0L;
private volatile long a0, a1, a2, a3, a4, a5, a6, a7 ;
private volatile long d4 = 0L;
}
执行结果:
11ms左右
方法2:在JAVA8中可以通过 @Contended注解
Contended注解可以用于类型上和属性上,加上这个注解之后虚拟机会自动进行填充,从而避免伪共享。
private static class CacheLineData {
@Contended
private volatile long d1 = 0L;
@Contended
private volatile long d2 = 0L;
@Contended
private volatile long d3 = 0L;
@Contended
private volatile long d4 = 0L;
}
要使Contended注解生效需要启动jvm时加入如下参数:
-XX:-RestrictContended
执行结果:
13ms左右
关于变量在其它core中是怎么失效的是通过MESI协议来完成的。