JAVA中的伪共享
Java中的伪共享
解决伪共享最直接的方法就是填充(padding),例如下面的VolatileLong,一个long占8个字节,Java的对象头占用8个字节(32位系统)或者12字节(64位系统,默认开启对象头压缩,不开启占16字节)。一个缓存行64字节,那么我们可以填充6个long(6 * 8 = 48 个字节)。
现在,我们学习JVM对象的内存模型。所有的Java对象都有8字节的对象头,前四个字节用来保存对象的哈希码和锁的状态,前3个字节用来存储哈希码,最后一个字节用来存储锁状态,一旦对象上锁,这4个字节都会被拿出对象外,并用指针进行链接。剩下4个字节用来存储对象所属类的引用。对于数组来讲,还有一个保存数组大小的变量,为4字节。每一个对象的大小都会对齐到8字节的倍数,不够8字节部分需要填充。为了保证效率,Java编译器在编译Java对象的时候,通过字段类型对Java对象的字段进行排序,如下表所示。
顺序 |
类型 |
字节数量 |
1 |
double |
8字节 |
2 |
long |
8字节 |
3 |
int |
4字节 |
4 |
float |
4字节 |
5 |
short |
2字节 |
6 |
char |
2字节 |
7 |
boolean |
1字节 |
8 |
byte |
1字节 |
9 |
对象引用 |
4字节或者8字节 |
10 |
子类字段 |
重新排序 |
因此,我们可以在任何字段之间通过填充长整型的变量把热点变量隔离在不同的缓存行中,通过减少伪同步,在多核心CPU中能够极大的提高效率。
最简单的方式
1 2 3 4 5 6 7 8 9
|
public class DataPadding { private long p1, p2, p3, p4, p5, p6; private long data; }
|
因为JDK1.7以后就自动优化代码会删除无用的代码,在JDK1.7以后的版本这些不生效了。
继承的方式
1 2 3 4 5 6 7
|
public class DataPadding { private long p1, p2, p3, p4, p5, p6; }
|
继承缓存填充类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public class VolatileData extends DataPadding { private long data = 0;
public VolatileData() { }
public VolatileData(long defValue) { this.data = defValue; }
public long accumulationAdd() { data++; return data; }
public long getValue() { return data; } }
|
这样在JDK1.8中是可以使用的
@Contended注解
1 2 3 4 5
| @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) public @interface Contended { String value() default ""; }
|
Contended注解可以用于类型上和属性上,加上这个注解之后虚拟机会自动进行填充,从而避免伪共享。这个注解在Java8 ConcurrentHashMap、ForkJoinPool和Thread等类中都有应用。我们来看一下Java8中ConcurrentHashMap中如何运用Contended这个注解来解决伪共享问题。以下说的ConcurrentHashMap都是Java8版本。
注意:在Java8中提供了**@sun.misc.Contended来避免伪共享时,在运行时需要设置JVM启动参数-XX:-RestrictContended**否则可能不生效。
缓存行填充的威力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
|
public class CacheLineTest {
private final boolean isDataPadding = false;
private volatile long x = 0; private volatile long y = 0; private volatile long z = 0;
private volatile VolatileData volatileDataX = new VolatileData(0); private volatile VolatileData volatileDataY = new VolatileData(0); private volatile VolatileData volatileDataZ = new VolatileData(0);
private final long size = 100000000;
public void accumulationX() { long currentTime = System.currentTimeMillis(); long value = 0; for (int i = 0; i < size; i++) { if (isDataPadding) { value = volatileDataX.accumulationAdd(); } else { value = (++x); }
} System.out.println(value); System.out.println("耗时:" + (System.currentTimeMillis() - currentTime)); }
public void accumulationY() { long currentTime = System.currentTimeMillis(); long value = 0; for (int i = 0; i < size; i++) { if (isDataPadding) { value = volatileDataY.accumulationAdd(); } else { value = ++y; }
} System.out.println(value); System.out.println("耗时:" + (System.currentTimeMillis() - currentTime)); }
public void accumulationZ() { long currentTime = System.currentTimeMillis(); long value = 0; for (int i = 0; i < size; i++) { if (isDataPadding) { value = volatileDataZ.accumulationAdd(); } else { value = ++z; } } System.out.println(value); System.out.println("耗时:" + (System.currentTimeMillis() - currentTime)); }
public static void main(String[] args) { CacheLineTest cacheRowTest = new CacheLineTest(); ExecutorService executorService = Executors.newFixedThreadPool(3); executorService.execute(() -> cacheRowTest.accumulationX()); executorService.execute(() -> cacheRowTest.accumulationY()); executorService.execute(() -> cacheRowTest.accumulationZ()); executorService.shutdown(); } }
|
不使用缓存行填充测试
1 2 3 4
|
private final boolean isDataPadding = false;
|
输出
1 2 3 4 5 6
| 100000000 耗时:7960 100000000 耗时:7984 100000000 耗时:7989
|
使用缓存行填充测试
1 2 3 4
|
private final boolean isDataPadding = true;
|
输出
1 2 3 4 5 6
| 100000000 耗时:176 100000000 耗时:178 100000000 耗时:182
|
同样的结构他们之间差了 将近 50倍的速度差距
总结
当多个线程同时对共享的缓存行进行写操作的时候,因为缓存系统自身的缓存一致性原则,会引发伪共享问题,解决的常用办法是将共享变量根据缓存行大小进行补充对齐,使其加载到缓存时能够独享缓存行,避免与其他共享变量存储在同一个缓存行。