伪共享
1 引用自:https://www.cnblogs.com/diegodu/p/9340243.html
2.7 Cache Line 伪共享
Cache Line 伪共享问题,就是由多个 CPU 上的多个线程同时修改自己的变量引发的。这些变量表面上是不同的变量,但是实际上却存储在同一条 Cache Line 里。
在这种情况下,由于 Cache 一致性协议,两个处理器都存储有相同的 Cache Line 拷贝的前提下,本地 CPU 变量的修改会导致本地 Cache Line 变成 Modified
状态,然后在其它共享此 Cache Line 的 CPU 上,
引发 Cache Line 的 Invaidate 操作,导致 Cache Line 变为 Invalidate
状态,从而使 Cache Line 再次被访问时,发生本地 Cache Miss,从而伤害到应用的性能。
在此场景下,多个线程在不同的 CPU 上高频反复访问这种 Cache Line 伪共享的变量,则会因 Cache 颠簸引发严重的性能问题。
下图即为两个线程间的 Cache Line 伪共享问题的示意图,
还记得我们讨论volatile(https://www.cnblogs.com/silyvin/p/9106786.html)时吗,说过volatile功能之一即是会强制刷新主存,并通知其它线程他们的缓存失效了(另一个是防止指令重排,其实还有一个,确保long及double的write原子)
2
3 手动伪共享方法 https://www.jianshu.com/p/c3c108c3dcfd
在Java 7之前,可以在属性的前后进行padding,例如:
public final static class VolatileLong {
volatile long p0, p1, p2, p3, p4, p5, p6;
public volatile long value = 0;
volatile long q0, q1, q2, q3, q4, q5, q6;
}
通过Java对象内存布局文章中结尾对paddign的分析可知,由于都是long类型的变量,这里就是按照声明的顺序分配内存,那么这可以保证在同一个缓存行中只有一个VolatileLong对象。
__ 这里有一个问题:据说Java7优化了无用字段,会使这种形式的补位无效,但经过测试,无论是在JDK 1.7 还是 JDK 1.8中,这种形式都是有效的。网上有关伪共享的文章基本都是来自Martin的两篇博客,这种优化方式也是在他的博客中提到的。但国内的文章貌似根本就没有验证过而直接引用了此观点,这也确实迷惑了一大批同学!__
4 jdk1.8注解 https://www.jianshu.com/p/c3c108c3dcfd
public class FakeShare {
public static void main(String []f) {
System.out.println(ClassLayout.parseInstance(new FakeShare.MyLong()).toPrintable());
}
private static class MyLong {
@Contended
public volatile long usefulVal;
public volatile long anotherVal;
}
}
markdown.FakeShare$MyLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) c2 c0 00 f8 (11000010 11000000 00000000 11111000) (-134168382)
12 4 (alignment/padding gap)
16 8 long MyLong.usefulVal 0
24 8 long MyLong.anotherVal 0
Instance size: 32 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
没有看到什么128字节的padding
// -XX:-RestrictContended
markdown.FakeShare$MyLong object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) c2 c0 00 f8 (11000010 11000000 00000000 11111000) (-134168382)
12 4 (alignment/padding gap)
16 8 long MyLong.anotherVal 0
24 128 (alignment/padding gap)
152 8 long MyLong.usefulVal 0
160 128 (loss due to the next object alignment)
Instance size: 288 bytes
Space losses: 132 bytes internal + 128 bytes external = 260 bytes total
可以看到在usefulVal前后各插入了一个128byte的padding gap
public final static class VolatileLong { public volatile long value = 0L; } // long padding避免false sharing // 按理说jdk7以后long padding应该被优化掉了,但是从测试结果看padding仍然起作用 public final static class VolatileLong2 { volatile long p0, p1, p2, p3, p4, p5, p6; public volatile long value = 0L; volatile long q0, q1, q2, q3, q4, q5, q6; } /** * jdk8新特性,Contended注解避免false sharing * Restricted on user classpath * Unlock: -XX:-RestrictContended */ @sun.misc.Contended public final static class VolatileLong3 { public volatile long value = 0L; }
下面看一下VolatileLong对象在运行时的内存大小(参考Java对象内存布局):
在堆内存中并没有看到对变量进行padding,大小与VolatileLong对象是一样的。
这就奇怪了,看起来与VolatileLong没什么不一样,但看一下内存的地址,用十六进制算一下,两个VolatileLong对象地址相差24字节,而两个VolatileLong3对象地址相差280字节。这就是前面提到的@sun.misc.Contended注解会在对象或字段的前后各增加128字节大小的padding,那么padding的大小就是256字节,再加上对象的大小24字节,结果就是280字节,所以确实是增加padding了。
6 相关java对象模型