Java也谈谈伪共享FlashSharing
1、什么是伪共享
- 在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素。
- 伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享(https://www.cnblogs.com/cyfonly/p/5800758.html)。
总之就是很麻烦,因为一个cpu的缓存问题,可以在高并发系统中让你的程序卡的要死.
伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行

伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行
-上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。
表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。
1-1 遭遇伪共享
package com.csdn.blog.xzknet.test.flashsharing;
public final class FalseSharingFail implements Runnable {
public final static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
static {
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
}
public FalseSharingFail(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
System.out.println("VolatileLong未解决了伪共享问题.所以每次longs[i]都会将其他的longs[i+1]或者longs[i-1]的64位加载进来.");
final long start = System.nanoTime();
runTest();
System.out.println("持续时间 = " + (System.nanoTime() - start));
}
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 FalseSharingFail(i));
}
// 启动所有线程
for (Thread t : threads) {
t.start();
}
// 等待所有线程停止
for (Thread t : threads) {
t.join();
}
}
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; // comment out
}
}
1-2 怎么避免伪共享
解决方法很简单,就是把连续的内存块儿给搞一些无用的引用之类的放上去,例如long占8个字节object引用占4个字节,总之,可能会出现伪共享的字段后你就使劲加吧加够64个字节就是了.例如上面的问题改造方法见下面的代码.
package com.csdn.blog.xzknet.test.flashsharing;
public final class FalseSharing implements Runnable {
public final static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
static {
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
}
public FalseSharing(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}
public static void main(final String[] args) throws Exception {
System.out.println("VolatileLong解决了伪共享问题.");
final long start = System.nanoTime();
runTest();
System.out.println("持续时间 = " + (System.nanoTime() - start));
}
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 FalseSharing(i));
}
// 启动所有线程
for (Thread t : threads) {
t.start();
}
// 等待所有线程停止
for (Thread t : threads) {
t.join();
}
}
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; // comment out
}
}
测试结果也给大家粘贴一下
VolatileLong未解决了伪共享问题.所以每次longs[i]都会将其他的longs[i+1]或者longs[i-1]的64位加载进来.
持续时间 = 45158255500
VolatileLong解决了伪共享问题.
持续时间 = 25888840119
差距还是很大的.尤其是在大并发的系统中,可能就是灾难性的.
2、总结
2-1 Java 6
解决伪共享的办法是使用缓存行填充,使一个对象占用的内存大小刚好为64bytes或它的整数倍,这样就保证了一个缓存行里不会有多个对象。《剖析Disruptor:为什么会这么快?(三)伪共享》提供了缓存行填充的例子:
public final static class VolatileLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}
2-2 Java 7
因此,JAVA 7下做缓存行填充更麻烦了,需要使用继承的办法来避免填充被优化掉
public class VolatileLongPadding {
public volatile long p1, p2, p3, p4, p5, p6; // 注释
}
public class VolatileLong extends VolatileLongPadding {
public volatile long value = 0L;
}
把padding放在基类里面,可以避免优化。(这好像没有什么道理好讲的,JAVA7的内存优化算法问题,能绕则绕)。
2-3 JAVA 8
中添加了一个@Contended的注解
import sun.misc.Contended;
@Contended
public class VolatileLong {
public volatile long value = 0L;
}
执行时,必须加上虚拟机参数-XX:-RestrictContended,@Contended注释才会生效。

浙公网安备 33010602011771号