伪共享

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原子)

 

 

 

 

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

在Java 8中,提供了@sun.misc.Contended注解来避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。具体可以参考http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html
 
 
 
 
5 实践
 
5.1 按https://www.jianshu.com/p/225e49257ad6的方式:
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

 
5.2 按https://www.jianshu.com/p/c3c108c3dcfd的补充,加上注解unlock
 
5.2.1 
// -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

 
5.2.2 使用dump看内存大小及内存地址
 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对象模型

 java object多大 java对象内存模型 数组有多长

posted on 2019-07-13 13:19  silyvin  阅读(308)  评论(0编辑  收藏  举报