原创

字段解析之伪共享(2)

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

Java8引入了@Contented注解来减少伪共享(False Sharing)的发生。在执行程序时,必须加上虚拟机参数-XX:-RestrictContended后注解才会生效。下面通过几个小实例来认识一下@Contended注解是如何影响对象在HotSpot中的内存布局的。

1、在类上应用@Contended注解:

@Contended
public static class ContendedTest2 {
        private Object plainField1;
        private Object plainField2;
        private Object plainField3;
        private Object plainField4;
}

以下是使用-XX:+PrintFieldLayout虚拟机命令输出的结果,如下:

TestContended$ContendedTest2: field layout
    Entire class is marked contended
    // @140表示字段在类中的地址偏移,通过对象头12字节加上128填充计算得来
     @140 --- instance fields start ---    
     @140 "plainField1" Ljava.lang.Object;
     @144 "plainField2" Ljava.lang.Object;
     @148 "plainField3" Ljava.lang.Object;
     @152 "plainField4" Ljava.lang.Object;
     @288 --- instance fields end ---
     @288 --- instance ends ---

注解将使整个字段块的两端都被填充。注意,我们使用了128字节的填充,这个数2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。

2、在字段上应用@Contended注解:

public static class ContendedTest1 {
        @Contended
        private Object contendedField1;
        private Object plainField1;
        private Object plainField2;
        private Object plainField3;
        private Object plainField4;
}

将导致该字段从连续的字段块中分离开来并高效的添加填充:

TestContended$ContendedTest1: field layout
     @ 12 --- instance fields start ---
     @ 12 "plainField1" Ljava.lang.Object;
     @ 16 "plainField2" Ljava.lang.Object;
     @ 20 "plainField3" Ljava.lang.Object;
     @ 24 "plainField4" Ljava.lang.Object;
     // 与普通的字段之间进行了填充,填充了128字节
     @156 "contendedField1" Ljava.lang.Object; (contended, group = 0)
     @288 --- instance fields end ---
     @288 --- instance ends ---

3、在多个字段上应用@Contended注解: 


public static class ContendedTest4 {
        @Contended
        private Object contendedField1;

        @Contended
        private Object contendedField2;

        private Object plainField3;
        private Object plainField4;
}

被注解的2个字段都被独立地填充:

TestContended$ContendedTest4: field layout
     @ 12 --- instance fields start ---
     @ 12 "plainField3" Ljava.lang.Object;
     @ 16 "plainField4" Ljava.lang.Object;
     // 与上一个字段之间填充了128字节
     @148 "contendedField1" Ljava.lang.Object; (contended, group = 0)
     // 与上一个字段之间填充了128字节
     @280 "contendedField2" Ljava.lang.Object; (contended, group = 0)
     @416 --- instance fields end ---
     @416 --- instance ends ---

4、应用@Contended注解进行字段分组

在有些情况下需要对字段进行分组,同一组的字段会和其他字段有访问冲突,但是和同一组的没有。例如,(同一个线程的)代码同时更新2个字段是很常见的情况。如果同时把2个字段都添加@Contended注解也行,但可以通过去掉他们之间的填充来提高内存空间的使用效率。

举例如下:

public static class ContendedTest5 {
        @Contended("updater1")
        private Object contendedField1;

        @Contended("updater1")
        private Object contendedField2;

        @Contended("updater2")
        private Object contendedField3;

        private Object plainField5;
        private Object plainField6;
}

内存布局如下:

TestContended$ContendedTest5: field layout
     @ 12 --- instance fields start ---
     @ 12 "plainField5" Ljava.lang.Object;
     @ 16 "plainField6" Ljava.lang.Object;
     // 与上一个字段之间填充了128字节
     @148 "contendedField1" Ljava.lang.Object; (contended, group = 12)
     @152 "contendedField2" Ljava.lang.Object; (contended, group = 12)
     // 与上一个字段之间填充了128字节
     @284 "contendedField3" Ljava.lang.Object; (contended, group = 15)
     @416 --- instance fields end ---
     @416 --- instance ends ---

可以看到contendedField1与contendedField2之间并没有填充128字节,因为这2个字段是属于同一组。  

正文到此结束