0%

伪共享

一、伪共享含义

根据《高速缓存》可知,高速缓存中的Cache Line缓存一定数量的字节,而不是只有1个字节。一般情况下,一个Cache Line有32或者64个字节。如果属于不同资源(比如“变量”)的字节被加载到同一个Cache Line,就导致出现了伪共享。

二、伪共享带来的问题

根据以上叙述,伪共享带来问题的本质叙述是:由于Cache Line缓存一定数量的字节,使得一部分原本不处于竞争关系的资源具有了竞争关系,而这最终导致了并发性能下降。
伪共享问题是底层硬件机制问题,故存在于所有程序语言中。

比如:

  • 假定变量a和b被加载到同一个Cache Line,在缓存锁定场景中,对a/b变量进行原子操作时,相应的Cache Line被缓存锁定,此时不能再并发修改和读取另外一个变量
  • 假定变量a和b被加载到同一个Cache Line,在MESI协议中,对a/b变量修改时,MESI协议要求锁住相应的Cache Line(需要注意的是,“锁住”与“缓存锁定”不同),此时不能再并发修改另外一个变量

三、伪共享带来的问题解决

对于伪共享带来的问题,常见的解决方案有:

  • 缓存行填充。缓存行填充的本质原理是,对于需要确保不分配在同一个Cache Line的两个资源,只需在它们中间填充字节,使得最后两者之间的字节距离大于一个Cache Line长度,此时可简单证明必然不会被分配到同一个Cache Line
  • 资源对齐。自动调整使得资源在Cache Line边界开始

伪共享问题的存在不可杜绝,解决伪共享问题需要一定成本,比如“Cache Line空间浪费”,“使用额外机制的复杂性引入”等,因此,是否解决伪共享问题需要具体情况具体分析,一般只有当收益远远大于成本时,才需要考虑解决伪共享问题,比如“一个队列数据结构,head和tail指针有大的并发修改概率”。

四、Java中伪共享问题解决

接下来的叙述以64位系统为环境,在64位系统中,一个Cache Line的长度为64字节。

4.1、解决方案

Java中对于伪共享问题的解决方案是“缓存行填充”,常见的具体手段有:“填充冗余变量”和“Java 8中引入的@sun.misc.Contended注解”。

在继续介绍之前,首先须明晰下Java内存结构与Cache Line的关系:

  • Java内存结构(包括“栈”、“Java堆”、“方法区”等)跟Cache Line没有关系,Cache Line只跟主存数据块有关
  • Java中两个变量是否会被加载到同一个Cache Line只由他们是否被分配到同一个主存数据块决定。围绕Java堆中Java对象(Java对象内存布局参见“备注”)内的变量进行说明:
    • 一个Java类A,其内只有一个long变量value,那么类A的实例对象在不考虑“对齐填充字节”的前提下占据24个字节(对象头16个字节,value变量8个字节)。围绕value变量,一个Cache Line最多可以加载3个value变量(1个不完整的类A实例对象,但加载了value部分,8到16个字节;2个完整的类A实例对象共48个字节),也可以是2个value变量,1个value变量,0个value变量
    • 一个Java类A,其内只有一个long变量value,一个m = new A[3]数组对象m,那么类A的实例对象在不考虑“对齐填充字节”的前提下占据24个字节(对象头16个字节,value变量8个字节),m在不考虑“对齐填充字节”的前提下占据32个字节(对象头20个字节,实例数据是3个引用变量12个字节)。围绕value变量和数组元素引用变量refer,一个Cache Line最多可以加载2个value变量和3个refer变量(1个不完整的类A实例对象,但加载了value部分,8个字节;1个完整的类A实例对象24个字节;1个完整的m数组对象32个字节)

备注
Java对象的内存布局由3部分组成:

  • 对象头。包含3部分:Mark Word(在64位系统中占据8字节),指向类元数据的指针(Klass Pointer,在64位系统中占据8字节),数组长度(在64位系统中占据4字节,只有数组对象才有该部分)
  • 实例数据
  • 对齐填充字节,Java对象大小要求是8字节的倍数,因此可能需要进行字节填充对齐

4.1.1、填充冗余变量

假定需要隔离的两个变量为a和b,那么可在a和b之间填充冗余变量。

上述方案的缺点是:

  • 耦合系统位数。在32和64位系统中,冗余变量填充所需的个数不一样。在32位系统中,Cache Line的长度为32字节,Java对象头所占据字节数分别为“Mark Word(4字节)”,“指向类的指针(4字节)”,“数组长度(4字节,只有数组对象才有该部分)”;在64位系统中,Cache Line的长度为64字节,Java对象头所占据字节数分别为“Mark Word(8字节)”,“指向类的指针(8字节)”,“数组长度(4字节,只有数组对象才有该部分)”
  • 一些JDK会智能去掉冗余变量,此时,该方案失效。故跟使用的JDK耦合

4.1.2、Java 8中引入的@sun.misc.Contended注解

通过使用该注解解决伪共享问题的原理是:运行时会分别在前后填充128字节。
注解的修饰目标有两个,分别是“类”和“类内成员变量”:

  • 类。在类内首个成员变量前填充128字节,在类内最后一个成员变量后填充128字节
  • 类内成员变量。在成员变量前填充128字节,在成员变量后填充128字节

备注:

  • 以上填充策略描述只是大体轮廓,还有很多具体细节未涉及

4.2、几个案例

1、案例1
著名的Java并发编程大师Doug lea在JDK 7的并发包里新增了一个队列类LinkedTransferQueue,它的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 队列中的头部节点 */
private transient final PaddedAtomicReference<QNode> head;

/** 队列中的尾部节点 */
private transient final PaddedAtomicReference<QNode> tail;

static final class PaddedAtomicReference <T> extends AtomicReference <T> {
// 使用很多4个字节的引用追加到64个字节
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
PaddedAtomicReference(T r) {
super(r);
}
}

public class AtomicReference <V> implements java.io.Serializable {
private volatile V value;
// 省略其他代码

在上述代码中,head和tail分别指向头部节点和尾部节点,不过需要注意的是,实际指向节点的引用变量是其中的value变量。当head和tail指向的对象相邻近时,head的value和tail的value很容易被加载到同一个Cache Line,在并发修改头节点和尾节点十分频繁的场景中,并发性能会大大降低,因此,通过填充冗余变量p0-pe(从理论上来说,其实无需填充15个冗余变量这么多,因为PaddedAtomicReference类的实例对象的对象头占据16个字节)的方式,使得两个value的字节距离必大于64个字节,从而确保两者不会被加载到同一个Cache Line。

2、案例2
高性能队列Disruptor中的RingBuffer类使用填充冗余变量的方式解决伪共享问题。
截取部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {
public static final long INITIAL_CURSOR_VALUE = -1L;
protected long p1;
protected long p2;
protected long p3;
protected long p4;
protected long p5;
protected long p6;
protected long p7;

// 省略其他代码

}

3、案例3
接下来进行一个实验,笔者机器的CPU配置是:1CPU,4核心,3.2GHz主频。
实验代码参考自网络:

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
public final class FalseSharing implements Runnable {
public final static long ITERATIONS = 500L * 1000L * 1000L;
public static int NUM_THREADS = 4; // change
private static VolatileLong[] longs;
private final int arrayIndex;

public FalseSharing(final int arrayIndex) {
this.arrayIndex = arrayIndex;
}

public static void main(final String[] args) throws Exception {
Thread.sleep(10000);
System.out.println("starting....");
if (args.length == 1) {
NUM_THREADS = Integer.parseInt(args[0]);
}

longs = new VolatileLong[NUM_THREADS];
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (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;
}
}

执行java FalseSharing,结果如下:

1
2
starting....
duration = 25836786319

即25秒左右,通过填充5个冗余变量的方式解决伪共享问题,VolatileLong类修改后的定义如下:

1
2
3
4
public final static class VolatileLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5;
}

现在两个实例对象内的value变量字节距离至少为64个字节(对象头16个字节,value变量8个字节,5个冗余变量40个字节),执行java -XX:-UseCompressedOops FalseSharing(这里需要加上“-XX:-UseCompressedOops”VM参数,否则上述64个字节距离的计算是错误的),结果如下:

1
2
starting....
duration = 15333024669

即15秒左右,通过填充6个冗余变量的方式解决伪共享问题,VolatileLong类修改后的定义如下:

1
2
3
4
public final static class VolatileLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6;
}

执行java FalseSharing,结果如下:

1
2
starting....
duration = 4591050110

即4秒左右,这里有个奇怪的现象就是:理论上来说,填充5个冗余变量的方式跟填充6个冗余变量的方式都解决了伪共享问题,但是后者却比前者大大提升了性能。笔者百思不得其解,请知道的读者不吝赐教。

接下来使用Contended注解的方式解决伪共享问题,VolatileLong类修改后的定义如下:

1
2
3
4
public final static class VolatileLong {
@Contended
public volatile long value = 0L;
}

执行java -XX:-RestrictContended FalseSharing(要想使用“@Contended”注解,须加上“-XX:-RestrictContended”VM参数),结果如下:

1
2
starting....
duration = 5072695821

参考文献

[1]https://blog.csdn.net/qq_27680317/article/details/78486220
[2]http://ifeve.com/from-javaeye-false-sharing/
[3]http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html
[4]https://blog.csdn.net/lkforce/article/details/81128115

您的支持将鼓励我继续分享!