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;
}
使用上述两种方法可以,把代码运行时间降低到几百毫秒
参考文档