• Java8的伪共享和缓存行填充--@Contended注释


    在我的前一篇文章<伪共享和缓存行填充,从Java 6, Java 7 到Java 8>中, 我们演示了在Java 8中,可以采用@Contended在类级别上的注释,来进行缓存行填充。这样,多线程情况下的伪共享冲突问题。 感兴趣的同学可以查看该文。

    其实,@Contended注释还可以应用于字段级别(Field-Level),当应用于字段级别时,被注释的字段将和其他字段隔离开来,会被加载在独立的缓存行上。在字段级别上,@Contended还支持一个“contention group”属性(Class-Level不支持),同一group的字段们在内存上将是连续,但和其他他字段隔离开来。

    上面只是泛泛的介绍一下。关于@Contended应用于Field-Level特别是contention group的相关的资料很少,源代码中的注释中有一些,还有关于JEP-142(即关于增加@Contended的提议)的邮件讨论组中的描述(http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html),其中的讲解是非常详细的(由于该讨论发生在@Contended实现的最初阶段,不能保证和现在的实现完全一致), 我摘录和翻译如下:

    @Contended注释的行为如下所示:

    A,在类上应用Contended:

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

    将使整个字段块的两端都被填充:(以下是使用 –XX:+PrintFieldLayout的输出)(翻译注:注意前面的@140表示字段在类中的地址偏移)

    TestContended$ContendedTest2: field layout
        Entire class is marked contended
         @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 ---

    注意,我们使用了128bytes的填充 -- 2倍于大多数硬件缓存行的大小 -- 来避免相邻扇区预取导致的伪共享冲突。

     

    B,在字段上应用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;
         @156 "contendedField1" Ljava.lang.Object; (contended, group = 0)
         @288 --- instance fields end ---
         @288 --- instance ends ---

    C, 注解多个字段使他们分别被填充:

    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;
         @148 "contendedField1" Ljava.lang.Object; (contended, group = 0)
         @280 "contendedField2" Ljava.lang.Object; (contended, group = 0)
         @416 --- instance fields end ---
         @416 --- instance ends ---

    在有些cases中,你会想对字段进行分组,同一组的字段会和其他字段有访问冲突,但是和同一组的没有。例如,(同一个线程的)代码同时更新2个字段是很常见的情况。如果同时把2个字段都添加@Contended注解是足够的(翻译注:但是太足够了),但我们可以通过去掉他们之间的填充,来优化它们的内存空间占用。为了区分组,我们有一个参数“contention group”来描述:

    所以:

    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;
         @148 "contendedField1" Ljava.lang.Object; (contended, group = 12)
         @152 "contendedField2" Ljava.lang.Object; (contended, group = 12)
         @284 "contendedField3" Ljava.lang.Object; (contended, group = 15)
         @416 --- instance fields end ---
         @416 --- instance ends ---
    注意$contendedField1 和$contendedField2和其他字段之间有填充,但是它们之间是紧挨着的。
     
     
    以上是对邮件组中大牛们原始实现解释的翻译。
     
    下面我们来做一个测试,看@Contended在字段级别,并且带分组的情况下,是否能解决伪缓存问题。
    import sun.misc.Contended;
    
    public class VolatileLong {
        @Contended("group0")
        public volatile long value1 = 0L;  
        @Contended("group0")
        public volatile long value2 = 0L;  
        
        @Contended("group1")
        public volatile long value3 = 0L;  
        @Contended("group1")
        public volatile long value4 = 0L;  
    }

     

    我们用2个线程来修改字段--

    测试1:线程0修改value1和value2;线程1修改value3和value4;他们都在同一组中。

    测试2:线程0修改value1和value3;线程1修改value2和value4;他们在不同组中。

     
    测试1:
    public final class FalseSharing implements Runnable {
        public final static long ITERATIONS = 500L * 1000L * 1000L;
        private static VolatileLong volatileLong;
        private String groupId;
    
        public FalseSharing(String groupId) {
            this.groupId = groupId;
    
        }
    
        public static void main(final String[] args) throws Exception {
            // Thread.sleep(10000);
            System.out.println("starting....");
    
            volatileLong = new VolatileLong();
            final long start = System.nanoTime();
            runTest();
            System.out.println("duration = " + (System.nanoTime() - start));
        }
    
        private static void runTest() throws InterruptedException {
            Thread t0 = new Thread(new FalseSharing("t0"));
            Thread t1 = new Thread(new FalseSharing("t1"));
            t0.start();
            t1.start();
            t0.join();
            t1.join();
        }
    
        public void run() {
            long i = ITERATIONS + 1;
            if (groupId.equals("t0")) {
                while (0 != --i) {
                    volatileLong.value1 = i;
                    volatileLong.value2 = i;
                }
            } else if (groupId.equals("t1")) {
                while (0 != --i) {
                    volatileLong.value3 = i;
                    volatileLong.value4 = i;
                }
            }
        }
    }
     
    测试2:(基于以上代码修改下面的部分)
    public void run() {
            long i = ITERATIONS + 1;
            if (groupId.equals("t0")) {
                while (0 != --i) {
                    volatileLong.value1 = i;
                    volatileLong.value3 = i;
                }
            } else if (groupId.equals("t1")) {
                while (0 != --i) {
                    volatileLong.value2 = i;
                    volatileLong.value4 = i;
                }
            }
        }

    测试1:

    starting....
    duration = 16821484056

    测试2:

    starting....
    duration = 39191867777

    可以看出,如果同一线程修改的是同一“contention group”中的字段,没有伪共享冲突,比有伪共享冲突的情况要快1倍多。

    后记:

    测试3:不使用@Contended

    public class VolatileLong {
        public volatile long value1 = 0L;  
        public volatile long value2 = 0L;  
        public volatile long value3 = 0L;  
        public volatile long value4 = 0L;  
    }

    结果:

    starting....
    duration = 38096777198

    参考:

    http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/sun/misc/Contended.java

    http://openjdk.java.net/jeps/142

    http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html

  • 相关阅读:
    JSP实现页面自动跳转
    marquee属性的使用说明
    jsp 按钮 超链接 直接跳转至另一页面
    生成验证码图片代码
    js 通过判断月数填充日数下拉框
    邮箱正则验证
    jsp 验证用正则表达式
    onselectstart 与 -moz-user-select
    onselectstart 、onselect区别
    NSOperation基本操作
  • 原文地址:https://www.cnblogs.com/Binhua-Liu/p/5623089.html
Copyright © 2020-2023  润新知