Disruptor以及@Contended注解

Posted 通凡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Disruptor以及@Contended注解相关的知识,希望对你有一定的参考价值。

Disruptor感想
很早之前阅读过Disruptor的使用,这里有篇美团团队的文章很详细的介绍了这种队列的相关原理以及为什么这么设计和这么设计为什么处理速度很快,https://tech.meituan.com/disruptor.html,不想再“拿来主义”,本篇文章只最为记录,同时针对文章中说的相关测试,这里我会将数据插入比较大的测试贴出来;其中对这篇文章中提到但是限于篇幅没有说明的java注解contended进行说明。

CAS的队列都是无界

这个与CAS的原理有关系,它是一个不断尝试的机制,直到返回最终的正确结果或者完成操作,其源码基本上都是这样的实现:

public final int getAndAdd(int delta) 
    for (;;) 
        int current = get();
        int next = current + delta;
        if (compareAndSet(current, next))
            return current;
    

如果有界应该会加锁处理边界问题。

缓存行实验

实验代码在原文中有,这里不再说明,只贴一下实验结果:

Loop times:10ms
Loop times:46ms

这里机器的配置3.2GHz,32G memory,机器配置的提升是实验数据提升的主要原因,其他实验的相关数据基本与原文中相似,这里不再赘述。

java 8 Contended注解

在Java 8中,可以采用@Contended在类级别上的注释,来进行缓存行填充。这样,可以解决多线程情况下的伪共享冲突问题。

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

@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 ---

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

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 ---

注意 c o n t e n d e d F i e l d 1 和 contendedField1 和 contendedField1contendedField2和其他字段之间有填充,但是它们之间是紧挨着的,类内偏移量为4 bytes,为一个对象的大小。

下面我们来做一个测试,看@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 Volatile Long 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

下面是我的测试数据:

测试1:

starting....
duration = 26198119279

测试2:

starting....
duration = 24851524100

我的结果不知道为什么没有提升一倍,困惑!(可以看出,如果同一线程修改的是同一“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 = 23347254719

以上是关于Disruptor以及@Contended注解的主要内容,如果未能解决你的问题,请参考以下文章

优化技术专题「线程间的高性能消息框架」终极关注Disruptor的核心源码和Java8的@Contended伪共享指南

Java8使用@sun.misc.Contended避免伪共享(False Sharing)

Thread 类中,@sun.misc.Contended("tlr") 里值 tlr 的含义

单机最快的队列Disruptor解析和使用

03_LongAdder 源码分析

Disruptor 线程间共享数据无需竞争