• Disruptor


    参考:https://blog.csdn.net/zhouzhenyong/article/details/81303011 ;https://www.jianshu.com/p/78160f213862

    1 简介

      Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题。
      与Kafka、RabbitMQ用于服务间的消息队列不同,disruptor一般用于线程间消息的传递。基于Disruptor开发的系统单线程能支撑每秒600万订单,

      Disruptor它是一个开源的并发框架,并得到2011 Duke’s 程序框架创新奖,可以在无锁的状况下实现网络的Queue并发操做。

      Disruptor是一个高性能的异步处理框架,或者能够认为是最快的消息框架(轻量的JMS),也能够认为是一个观察者模式的实现。

      disruptor能够理解为是一种高效的"生产者-消费者"模型。也就性能远远高于传统的BlockingQueue容器。BlockingQueue实现了生产者-消费者模型,但是BlockingQueue是基于锁实现的, 而锁的效率一般较低. 

      Disruptor使用观察者模式, 主动将消息发送给消费者, 而不是等消费者从队列中取; 在无锁的状况下(采用CAS), 实现queue(环形, RingBuffer)的并发操做, 性能远高于BlockingQueue

     

    2 队列ArrayBlockingQueue缺点

      队列的底层数据结构一般分成三种:数组、链表和堆。其中,堆这里是为了实现带有优先级特性的队列,暂且不考虑。
      在稳定性和性能要求特别高的系统中,为了防止生产者速度过快,导致内存溢出,只能选择有界队列;同时,为了减少Java的垃圾回收对系统性能的影响,会尽量选择array/heap格式的数据结构。

      符合条件的队列就只有ArrayBlockingQueue。但是ArrayBlockingQueue是通过加锁的方式保证线程安全,而且ArrayBlockingQueue还存在伪共享问题,这两个问题严重影响了性能。


    3 性能对比

      disruptor是用于一个JVM中多个线程之间的消息队列,作用与ArrayBlockingQueue有相似之处,但是disruptor从功能、性能都远好于ArrayBlockingQueue,当多个线程之间传递大量数据或对性能要求较高时,可以考虑使用disruptor作为ArrayBlockingQueue的替代者。
     官方也对disruptor和ArrayBlockingQueue的性能在不同的应用场景下做了对比,本文列出其中一组数据,数据中P代表producer,C代表consumer,ABS代表ArrayBlockingQueue,目测性能只有有5~10倍左右的提升

     

    4 伪共享概念

    4.1 计算机缓存

    计算机早就支持多核,软件也越来越多的支持多核运行,其实也可以叫做多处理运行。一个处理器对应一个物理插槽。其中一个插槽对应一个L3 Cache,一个槽包含多个cpu。一个cpu包含寄存器、L1 Cache、L2 Cache,如下图所示

      其中越靠近cpu则,速度越快,容量则越小。

      其中L1和L2是只能给一个cpu进行共享,但是L3是可以给同一个槽内的cpu共享,而主内存,是可以给所有的cpu共享,这就是内存的共享。
      其中cpu执行运算的流程是这样:首先回去L1里面查找对应数据,如果没有则去L2、L3,如果都没有,则就会去主内存中去拿,走的路越长,则耗费时间越久,性能就会越低。
      需要注意的是,当线程之间进行共享数据的,需要将数据写回到主内存中,而另一个线程通过访问主内存获得新的数据。
      有人就会问了,多个线程之间不是会有一些非主内存的缓存进行共享么,那么另外一个线程会不会直接访问到修改之前的内存呢。答案是会的,但是有一点,就是这种数据我们可以通过设置缓存失效策略来进行保证缓存的最新,这个方式其实在cpu这里进行设置的,叫内存屏障(其实就是在cpu这里设置一条指令,这个指令就是禁止cpu重排序,这个屏障之前的不能出现在屏障之后,屏障之后的处理不能出现屏障之前,也就是屏障之后获取到的数据是最新的),对应到应用层面就是一个关键字volatile。

    4.2 缓存行的概念

      缓存是由很多个Cache line 组成的,每个缓存行大小是32~128字节(通常是64字节)。我们这里假设缓存行是64字节,而java的一个Long类型是8字节,这样的话一个缓存行就可以存8个Long类型的变量,如下图所示

       cpu 每次从主内存中获取数据的时候都会将相邻的数据存入到同一个缓存行中。假设我们访问一个Long内存对应的数组的时候,如果其中一个被加载到内存中,那么对应的后面的7个数据也会被加载到对应的缓存行中,这样就会非常快的访问数据

     

    4.3 伪共享

      缓存的失效其实就是缓存行的失效,缓存行失效的原理是什么,这里又涉及到一个MESI协议(缓存一致性协议),我们这里不介绍这个,我们只需知道在一个缓存行中的数据变化的时候,其他所有缓存中的这个缓存行都会失效

      我们用Disruptor中很经典的讲解伪共享的图来讲解下

      上图中显示的是一个槽的情况,里面是多个cpu, 如果cpu1上面的线程更新了变量X,根据MESI协议,那么变量X对应的所有缓存行都会失效(注意:虽然改的是X,但是X和Y被放到了一个缓存行,就一起失效了),这个时候如果cpu2中的线程进行读取变量Y,发现缓存行失效,想获取Y就会按照缓存查找策略,往上查找,如果期间cpu1对应的线程更新X后没有访问X(也就是没有刷新缓存行),cpu2的线程就只能从主内存中获取数据,对性能就会造成很大的影响,这就是伪共享。
      表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

    5  ArrayBlockingQueue 的伪共享问题

        public void put(E e) throws InterruptedException {
            checkNotNull(e);
            final ReentrantLock lock = this.lock;
            //获取当前对象锁
            lock.lockInterruptibly();
            try {
                while (count == items.length)
                    //阻塞并释放锁,等待notFull.signal()通知
                    notFull.await();
                //将数据放入数组
                enqueue(e);
            } finally {
                lock.unlock();
            }
        }
        private void enqueue(E x) {
            final Object[] items = this.items;
            //putIndex 就是入队的下标
            items[putIndex] = x;
            if (++putIndex == items.length)
                putIndex = 0;
            count++;
            notEmpty.signal();
        }
    public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            //加锁
            lock.lockInterruptibly();
            try {
                while (count == 0)
                    //阻塞并释放对象锁,并等待notEmpty.signal()通知
                    notEmpty.await();
                //在数据不为空的情况下
                return dequeue();
            } finally {
                lock.unlock();
            }
        }
    private E dequeue() {
        final Object[] items = this.items;
        //takeIndex 是出队的下标
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

    其中最核心的三个成员变量为
    putIndex:入队下标
    takeIndex:出队下标
    count:队列中元素的数量
    而三个成员的位置如下:

      这三个变量很容易放到同一个缓存行中,伪共享

     

    6 Disruptor原理介绍

      上面说了队列的两个性能问题:一个是加锁,一个是伪共享,那么disruptor是怎么解决这两个问题的,以及除了解决这两个问题之外,还引入了其他什么先进的东西提升性能的。

      1)引入环形的数组结构:预分配内存,Disruptor初始化的时候就创建空消息对象放进数组,生产者生产消息,实际上是更新消息对象。生产者添加元素加了一圈回到开头的时候,直接覆盖原来的消息(实际上也是更新),数组元素不会被回收,避免频繁的GC
      2)无锁的设计:采用CAS无锁方式,保证线程的安全性
      3)属性填充:通过添加额外的无用信息,避免伪共享问题
      4)元素位置的定位:维护一个索引sequence,进行自增,表示元素已经写到了哪个位置

    6.1 环形数组结构

      在使用队列的时候,一般有两种数据结构,数组和链表。就遍历来说,数组的速度肯定是远大于链表的。我们可以看jdk给我们提供的容器,BlockinQuene是阻塞的,如果不要阻塞,可以使用ConcurrentLinkedQuene。但是我们找不到ConcurrentArrayQuene,JDK没有提供。为什么呢?因为数组的大小是固定的,每次往里面放元素都是需要拷贝,这个效率是非常低的。

      所以有了Disruptor,它采用环形数组。相比于链表需要维护头指针和尾指针(头部拿元素需要加锁,尾部加元素也需要加锁),它只维护一个位置sequence。

      而且,你可以为数组预先分配内存(Disruptor初始化的时候就创建空对象放进数组),使得数组对象一直存在(除非程序终止),生产消息的时候只需要更新对象消息即可。这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。环形数组中的元素采用覆盖方式,避免了jvm的GC。

      而且根据我们对上面缓存行的解释知道,数组中的一个元素加载,相邻的数组元素也是会被预加载的,因此在这样的结构中,cpu无需时不时去主存加载数组中的下一个元素。
      其次结构作为环形,数组的大小为2的n次方,这样元素定位可以通过位运算效率会更高,这个跟一致性哈希中的环形策略有点像。在disruptor中,这个牛逼的环形结构就是RingBuffer,既然是数组,那么就有大小,而且这个大小必须是2的n次方,结构如下。

      

       实质只是一个普通的数组,只是当放置数据填充满队列(即到达2^n-1位置)之后,再填充数据,就会从0开始,覆盖之前的数据,于是就相当于一个环

     

    6.2 指针

      RingBuffer的指针(Sequence)属于一个volatile变量,用以指向下一个可用的元素,供生产者与消费者使用。

      同时也是我们能够不用锁操作就能实现Disruptor的原因之一,而且Sequence通过padding来避免伪共享。

      该所谓指针是通过一直自增的方式来获取下一个可写或者可读数据位置(通过位运算对环形数组大小取余)

      

     

    6.3 生产者的模式

      Disruptor生产者分为单线程模式和多线程模式(默认的)。单线程模式生产者对于sequence的访问是不加锁。

      在new Disruptor的时候可以指定。

      如果确定是单线程环境,可以指定为单线程模式,因为它是不加锁的,效率更高。但是,一定要保证确实是单线程的,否则会有线程安全问题。

     

    6.4 消费者

      可以指定多个消费者。每个消费者都是独立的线程。

    6.5 生产和消费

    6.5.1 生产

      生产者写入数据的步骤包括:

        占位;

        移动游标并填充数据;

      1)如何避免生产者的生产速度过快而造成的新消息覆盖了未被消费的旧消息的问题;
        

      2)如何解决多个生产者抢占生产位的问题;
        多个生产者通过CAS获取生产位;

     

    6.5.2 消费

      一个消费者一个线程;

      每个消费者都有一个游标表示已经消费到哪了(Sequence);

      消息者会等待(waitFor)新数据,直到生产者通知(signal);

      1)如何防止读取的时候,读到还未写的元素?

      2)怎么避免重复消费,比如数组大小为8,生产者生产了8个消息就没有新的消息了,几个消费者去消费,消费了一圈后,怎么防止继续消费下去

     

    6.6 消费异常处理

      消费者出现异常后,我们可以自定义怎么样去处理

     

            disruptor.handleExceptionsFor(消费者对象).with(new ExceptionHandler<MsgEvent>() {
                    @Override
                    public void handleEventException(Throwable ex, long sequence, MsgEvent event) {
                        System.out.println("出错啦handleEventException");
                    }
    
                    @Override
                    public void handleOnStartException(Throwable ex) {
                        System.out.println("出错啦handleOnStartException");
                    }
    
                    @Override
                    public void handleOnShutdownException(Throwable ex) {
                        System.out.println("出错啦handleOnShutdownException");
                    }
                });

     

    6.7 等待策略

      那么如何保证生产的消息不会覆盖没有消费掉的消息呢。

      Disruptor是有等待策略的,在添加消息前,会判断这个位置原来的消息对象的状态,若是这个消息还没有被消费,根据不同的策略,会采取不同的措施。具体策略下面会做介绍

      1)BlockingWaitStrategy(常用):默认策略。通过线程阻塞的方式,生产者等待被唤醒,被唤醒后,再检查依赖的sequence是否被消费
      2)BusySpinWaitStrategy:持续自旋,比较消耗cpu
      3)LiteBlockingWaitStrategy:通过线程阻塞的方式,基于BlockingWaitStrategy,和BlockingWaitStrategy相比,在没有锁竞争的时候会省去唤醒操作,但是作者说测试不充分,不建议使用
      4)TimeoutBlockingWaitStrategy:相较于BlockingWaitStrategy,有超时时间,超时后会执行业务指定的处理逻辑
      5)LiteTimeoutBlockingWaitStrategy:基于TimeoutBlockingWaitStrategy,在没有锁竞争的时候会省去唤醒操作
      6)SleepingWaitStrategy(常用):三段式,第一阶段自旋,第二阶段执行Thread.yield交出CPU,第三阶段睡眠一段时间时间,看看是否被消费,没有就继续睡眠
      7)YieldingWaitStrategy(常用):二段式,第一阶段自旋,第二阶段执行Thread.yield交出CPU
      8)PhasedBackoffWaitStrategy:四段式,第一阶段自旋指定次数,第二阶段自旋指定时间,第三阶段执行Thread.yield交出CPU,第四阶段调用成员变量的waitFor方法,这个成员变量可以设置为BlockingWaitStrategy、LiteBlockingWaitStrategy、SleepingWaitStrategy这三个中的一个

     

    6.8 原理图

     

    7 基本使用

      1)定义一个消息Event

      2)定义一个消息工厂

      3)定义消费者EventHandler

      

    class DisrupterTest {
    
    
        static class MsgEvent {
    
            private long value;
    
            public long getValue() {
                return value;
            }
    
            public void setValue(long value) {
                this.value = value;
            }
        }
    
        //消息工厂
        static class LongEventFactory implements EventFactory<MsgEvent> {
    
            public MsgEvent newInstance() {
    
                return new MsgEvent();
            }
    
        }
    
        //消费者
        static class MsgConsumer implements EventHandler<MsgEvent>{
            private String name;
            public MsgConsumer(String name){
                this.name = name;
            }
    
            @Override
            public void onEvent(MsgEvent event, long sequence, boolean endOfBatch) throws Exception {
                Thread.sleep(100);
                System.out.println(this.name+" -> 接收到信息: "+ event.getValue());
            }
        }
    
    
    
        //生产者处理
        static class MsgProducer {
    
            private Disruptor disruptor;
    
            public MsgProducer(Disruptor disruptor){
                this.disruptor = disruptor;
            }
    
            public void send(long data){
                RingBuffer<MsgEvent> ringBuffer = this.disruptor.getRingBuffer();
                long next = ringBuffer.next();
                try{
                    MsgEvent event = ringBuffer.get(next);
                    event.setValue(data);
                }finally {
                    ringBuffer.publish(next);
                }
            }
    
            public void sendList(List<Long> dataList){
                dataList.stream().forEach(data -> this.send(data));
            }
        }
    
    
        //触发测试
        static class DisruptorDemo {
            public void test(){
                Disruptor<MsgEvent> disruptor = new Disruptor<MsgEvent>(new LongEventFactory(), 1024, Executors.defaultThreadFactory());
    
                //定义三个消费者
                MsgConsumer msg1 = new MsgConsumer("消费者A");
                MsgConsumer msg2 = new MsgConsumer("消费者B");
                MsgConsumer msg3 = new MsgConsumer("消费者C");
    
                //绑定配置关系
                disruptor.handleEventsWith( msg1,msg2,msg3);
    
                //定义某个消费者出现异常后,怎么处理
                disruptor.handleExceptionsFor(msg1).with(new ExceptionHandler<MsgEvent>() {
                    @Override
                    public void handleEventException(Throwable ex, long sequence, MsgEvent event) {
                        System.out.println("出错啦handleEventException");
                    }
    
                    @Override
                    public void handleOnStartException(Throwable ex) {
                        System.out.println("出错啦handleOnStartException");
                    }
    
                    @Override
                    public void handleOnShutdownException(Throwable ex) {
                        System.out.println("出错啦handleOnShutdownException");
                    }
                });
    
                disruptor.start();
                // 定义要发送的数据
                MsgProducer msgProducer = new MsgProducer(disruptor);
    
                msgProducer.sendList(Arrays.asList(1L,2L,3L,4L,5L));
    
    
                //关闭disruptor
                disruptor.shutdown();
    
            }
        }
    
        public static void main(String[] args) {
            DisruptorDemo d = new DisruptorDemo();
            d.test();
        }
    }

    执行结果

    消费者B -> 接收到信息: 1
    消费者C -> 接收到信息: 1
    消费者A -> 接收到信息: 1
    消费者B -> 接收到信息: 2
    消费者A -> 接收到信息: 2
    消费者C -> 接收到信息: 2
    消费者C -> 接收到信息: 3
    消费者B -> 接收到信息: 3
    消费者A -> 接收到信息: 3
    消费者B -> 接收到信息: 4
    消费者A -> 接收到信息: 4
    消费者C -> 接收到信息: 4
    消费者C -> 接收到信息: 5
    消费者A -> 接收到信息: 5
    消费者B -> 接收到信息: 5

     

    8 高级使用

    8.1 单一写者模式

        在并发系统中提高性能最好的方式之一就是单一写者原则,对Disruptor也是适用的。如果在你的代码中仅仅有一个事件生产者,那么可以设置为单一生产者模式来提高系统的性能(默认)。

     

    8.2 串行消费

    现在触发一个注册Event,需要有一个Handler来存储信息,一个Hanlder来发邮件等等

    /**
      * 串行依次执行
      * <br/>
      * p --> c11 --> c21
      * @param disruptor
      */
     public static void serial(Disruptor<LongEvent> disruptor){
         disruptor.handleEventsWith(new C11EventHandler()).then(new C21EventHandler());
         disruptor.start();
     }

     

    8.3 菱形方式执行

     

     

     public static void diamond(Disruptor<LongEvent> disruptor){
         disruptor.handleEventsWith(new C11EventHandler(),new C12EventHandler()).then(new C21EventHandler());
         disruptor.start();
     }

     

    8.4 链式并行计算

     

     public static void chain(Disruptor<LongEvent> disruptor){
         disruptor.handleEventsWith(new C11EventHandler()).then(new C12EventHandler());
         disruptor.handleEventsWith(new C21EventHandler()).then(new C22EventHandler());
         disruptor.start();
     }

     

    8.5 相互隔离模式

     public static void parallelWithPool(Disruptor<LongEvent> disruptor){
         disruptor.handleEventsWithWorkerPool(new C11EventHandler(),new C11EventHandler());
         disruptor.handleEventsWithWorkerPool(new C21EventHandler(),new C21EventHandler());
         disruptor.start();
     }

     

    8.6 航道模式

    /**
      * 串行依次执行,同时C11,C21分别有2个实例
       * <br/>
       * p --> c11 --> c21
       * @param disruptor
       */
      public static void serialWithPool(Disruptor<LongEvent> disruptor){
          disruptor.handleEventsWithWorkerPool(new C11EventHandler(),new C11EventHandler()).then(new C21EventHandler(),new C21EventHandler());
          disruptor.start();
      }

     

  • 相关阅读:
    [剑指 Offer 18. 删除链表的节点]
    [922. 按奇偶排序数组 II]
    [905. 按奇偶排序数组]
    Linux信号机制
    [1470. 重新排列数组]
    linux常用命令全称
    pidof查看服务的PID
    运行shell脚本提示syntax error near unexpected token `$'do ''
    influxdb安装
    jvm堆内存设置问题Java heap space、GC overhead limit exceeded
  • 原文地址:https://www.cnblogs.com/jthr/p/16788796.html
Copyright © 2020-2023  润新知