Disruptor入门

 

获得Disruptor

可以通过Maven或者下载jar来安装Disruptor。只要把对应的jar放在Java classpath就可以了。

基本的事件生产和消费

我们从一个简单的例子开始学习Disruptor:生产者传递一个long类型的值给消费者,而消费者消费这个数据的方式仅仅是把它打印出来。首先声明一个Event来包含需要传递的数据:

 1 public class LongEvent {
2 private long value;
3 public long getValue() {
4 return value;
5 }
6
7 public void setValue(long value) {
8 this.value = value;
9 }
10 }

由于需要让Disruptor为我们创建事件,我们同时还声明了一个EventFactory来实例化Event对象。

1 public class LongEventFactory implements EventFactory {
2 @Override
3 public Object newInstance() {
4 return new LongEvent();
5 }
6 }

我们还需要一个事件消费者,也就是一个事件处理器。这个事件处理器简单地把事件中存储的数据打印到终端:

1 /**
2 */public class LongEventHandler implements EventHandler<LongEvent> {
3 @Override
4 public void onEvent(LongEvent longEvent, long l, boolean b) throws Exception {
5 System.out.println(longEvent.getValue());
6 }
7 }

事件都会有一个生成事件的源,这个例子中假设事件是由于磁盘IO或者network读取数据的时候触发的,事件源使用一个ByteBuffer来模拟它接受到的数据,也就是说,事件源会在IO读取到一部分数据的时候触发事件(触发事件不是自动的,程序员需要在读取到数据的时候自己触发事件并发布):

 1 public class LongEventProducer {
2 private final RingBuffer<LongEvent> ringBuffer;
3 public LongEventProducer(RingBuffer<LongEvent> ringBuffer) {
4 this.ringBuffer = ringBuffer;
5 }
6
7 /**
8 * onData用来发布事件,每调用一次就发布一次事件事件
9 * 它的参数会通过事件传递给消费者
10 *
11 * @param bb
12 */public void onData(ByteBuffer bb) {
13 //可以把ringBuffer看做一个事件队列,那么next就是得到下面一个事件槽
14 long sequence = ringBuffer.next();try {
15 //用上面的索引取出一个空的事件用于填充
16 LongEvent event = ringBuffer.get(sequence);// for the sequence
17 event.setValue(bb.getLong(0));
18 } finally {
19 //发布事件
20 ringBuffer.publish(sequence);
21 }
22 }
23 }

很明显的是:当用一个简单队列来发布事件的时候会牵涉更多的细节,这是因为事件对象还需要预先创建。发布事件最少需要两步:获取下一个事件槽并发布事件(发布事件的时候要使用try/finnally保证事件一定会被发布)。如果我们使用RingBuffer.next()获取一个事件槽,那么一定要发布对应的事件。如果不能发布事件,那么就会引起Disruptor状态的混乱。尤其是在多个事件生产者的情况下会导致事件消费者失速,从而不得不重启应用才能会恢复。

Disruptor 3.0提供了lambda式的API。这样可以把一些复杂的操作放在Ring Buffer,所以在Disruptor3.0以后的版本最好使用Event Publisher或者Event Translator来发布事件。

 1 public class LongEventProducerWithTranslator {
2 //一个translator可以看做一个事件初始化器,publicEvent方法会调用它
3 //填充Event
4 private static final EventTranslatorOneArg<LongEvent, ByteBuffer> TRANSLATOR =
5 new EventTranslatorOneArg<LongEvent, ByteBuffer>() {
6 public void translateTo(LongEvent event, long sequence, ByteBuffer bb) {
7 event.setValue(bb.getLong(0));
8 }
9 };
10 private final RingBuffer<LongEvent> ringBuffer;
11 public LongEventProducerWithTranslator(RingBuffer<LongEvent> ringBuffer) {
12 this.ringBuffer = ringBuffer;
13 }
14
15 public void onData(ByteBuffer bb) {
16 ringBuffer.publishEvent(TRANSLATOR, bb);
17 }
18 }

上面写法的另一个好处是,Translator可以分离出来并且更加容易单元测试。Disruptor提供了不同的接口(EventTranslator, EventTranslatorOneArg, EventTranslatorTwoArg, 等等)去产生一个Translator对象。很明显,Translator中方法的参数是通过RingBuffer来传递的。

最后一步就是把所有的代码组合起来完成一个完整的事件处理系统。Disruptor在这方面做了简化,使用了DSL风格的代码(其实就是按照直观的写法,不太能算得上真正的DSL)。虽然DSL的写法比较简单,但是并没有提供所有的选项。如果依靠DSL已经可以处理大部分情况了。

public class LongEventMain {
public static void main(String[] args) throws InterruptedException {
// Executor that will be used to construct new threads for consumers
Executor executor = Executors.newCachedThreadPool();
// The factory for the event
LongEventFactory factory = new LongEventFactory();
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024;
// Construct the Disruptor
Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>(factory, bufferSize, executor);
// Connect the handler
disruptor.handleEventsWith(new LongEventHandler());
// Start the Disruptor, starts all threads running
disruptor.start();
// Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); LongEventProducer producer = new LongEventProducer(ringBuffer); ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++) {
bb.putLong(0, l);
producer.onData(bb);
Thread.sleep(1000);
}
}
}

使用Java 8

Disruptor在自己的接口里面添加了对于Java 8 Lambda的支持。大部分Disruptor中的接口都符合Functional Interface的要求(也就是在接口中仅仅有一个方法)。所以在Disruptor中,可以广泛使用Lambda来代替自定义类。

 1 public class LongEventMainJava8 {
2 /**
3 * 用lambda表达式来注册EventHandler和EventProductor
4 * @param args
5 * @throws InterruptedException
6 */public static void main(String[] args) throws InterruptedException {
7 // Executor that will be used to construct new threads for consumers
8 Executor executor = Executors.newCachedThreadPool();
9 // Specify the size of the ring buffer, must be power of 2.
10 int bufferSize = 1024;// Construct the Disruptor
11 Disruptor<LongEvent> disruptor = new Disruptor<>(LongEvent::new, bufferSize, executor);
12 // 可以使用lambda来注册一个EventHandler
13 disruptor.handleEventsWith((event, sequence, endOfBatch) -> System.out.println("Event: " + event.getValue()));
14 // Start the Disruptor, starts all threads running
15 disruptor.start();
16 // Get the ring buffer from the Disruptor to be used for publishing.
17 RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
18
19 LongEventProducer producer = new LongEventProducer(ringBuffer);
20
21 ByteBuffer bb = ByteBuffer.allocate(8);for (long l = 0; true; l++) {
22 bb.putLong(0, l);
23 ringBuffer.publishEvent((event, sequence, buffer) -> event.setValue(buffer.getLong(0)), bb);
24 Thread.sleep(1000);
25 }
26 }
27 }

在上面的代码中,有很多自定义类型可以被省略了。还有注意的是:publishEvent方法中仅调用传递给它的参数,并不是直接调用对应的对象。如果把这段代码换成下面的代码:

1 ByteBuffer bb = ByteBuffer.allocate(8);
2 for (long l = 0; true; l++)
3 {
4 bb.putLong(0, l);
5 ringBuffer.publishEvent((event, sequence) -> event.set(bb.getLong(0)));
6 Thread.sleep(1000);
7 }

这段代码中有一个捕获参数的lambda,意味着在lambda表达式生成的内部类中会生成一个对象来存储这个捕获的bb对象。这会增加不必要的GC。所以在需要较低GC水平的情况下最好把所有的参数都通过publishEvent传递。

由于在Java 8中方法引用也是一个lambda,因此还可以把上面的代码改成下面的代码:

 1 public class LongEventWithMethodRef {
2 public static void handleEvent(LongEvent event, long sequence, boolean endOfBatch)
3 {
4 System.out.println(event.getValue());
5 }
6
7 public static void translate(LongEvent event, long sequence, ByteBuffer buffer)
8 {
9 event.setValue(buffer.getLong(0));
10 }
11
12 public static void main(String[] args) throws Exception
13 {
14 // Executor that will be used to construct new threads for consumers
15 Executor executor = Executors.newCachedThreadPool();
16 // Specify the size of the ring buffer, must be power of 2.
17 int bufferSize = 1024;
18 // Construct the Disruptor
19 Disruptor<LongEvent> disruptor = new Disruptor<>(LongEvent::new, bufferSize, executor);
20 // Connect the handler
21 disruptor.handleEventsWith(LongEventWithMethodRef::handleEvent);
22 // Start the Disruptor, starts all threads running
23 disruptor.start();
24 // Get the ring buffer from the Disruptor to be used for publishing.
25 RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
26
27 LongEventProducer producer = new LongEventProducer(ringBuffer);
28
29 ByteBuffer bb = ByteBuffer.allocate(8);
30 for (long l = 0; true; l++)
31 {
32 bb.putLong(0, l);
33 ringBuffer.publishEvent(LongEventWithMethodRef::translate, bb);
34 Thread.sleep(1000);
35 }
36 }
37 }
38

基本调整选项

上面的代码已经可以处理大多数的情况了,但是在有的时候还是会需要根据不同的软件或者硬件来调整选项以获得更高的性能。基本的选项有两个:单或者多生产者模式和可选的等待策略。

单或多 事件生产者

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

 1 public class singleProductorLongEventMain {
2 public static void main(String[] args) throws Exception {
3 //.....// Construct the Disruptor with a SingleProducerSequencer
4
5 Disruptor<LongEvent> disruptor = new Disruptor(factory,
6 bufferSize,
7 ProducerType.SINGLE, // Single producernew BlockingWaitStrategy(),
8 executor);//.....
9 }
10 }

为了证明,下面的数据是从Mac Air i7上面测试的结果:

多生产者:

Run 0, Disruptor=26,553,372 ops/sec
Run 1, Disruptor=28,727,377 ops/sec
Run 2, Disruptor=29,806,259 ops/sec
Run 3, Disruptor=29,717,682 ops/sec
Run 4, Disruptor=28,818,443 ops/sec
Run 5, Disruptor=29,103,608 ops/sec
Run 6, Disruptor=29,239,766 ops/sec

单生产者:

Run 0, Disruptor=89,365,504 ops/sec
Run 1, Disruptor=77,579,519 ops/sec
Run 2, Disruptor=78,678,206 ops/sec
Run 3, Disruptor=80,840,743 ops/sec
Run 4, Disruptor=81,037,277 ops/sec
Run 5, Disruptor=81,168,831 ops/sec
Run 6, Disruptor=81,699,346 ops/sec

 可选的等待策略

Disruptor默认的等待策略是BlockingWaitStrategy。这个策略的内部适用一个锁和条件变量来控制线程的执行和等待(Java基本的同步方法)。BlockingWaitStrategy是最慢的等待策略,但也是CPU使用率最低和最稳定的选项。然而,可以根据不同的部署环境调整选项以提高性能。

SleepingWaitStrategy

和BlockingWaitStrategy一样,SpleepingWaitStrategy的CPU使用率也比较低。它的方式是循环等待并且在循环中间调用LockSupport.parkNanos(1)来睡眠,(在Linux系统上面睡眠时间60µs).然而,它的优点在于生产线程只需要计数,而不执行任何指令。并且没有条件变量的消耗。但是,事件对象从生产者到消费者传递的延迟变大了。SleepingWaitStrategy最好用在不需要低延迟,而且事件发布对于生产者的影响比较小的情况下。比如异步日志功能。

YieldingWaitStrategy

YieldingWaitStrategy是可以被用在低延迟系统中的两个策略之一,这种策略在减低系统延迟的同时也会增加CPU运算量。YieldingWaitStrategy策略会循环等待sequence增加到合适的值。循环中调用Thread.yield()允许其他准备好的线程执行。如果需要高性能而且事件消费者线程比逻辑内核少的时候,推荐使用YieldingWaitStrategy策略。例如:在开启超线程的时候。

BusySpinWaitStrategy

BusySpinWaitStrategy是性能最高的等待策略,同时也是对部署环境要求最高的策略。这个性能最好用在事件处理线程比物理内核数目还要小的时候。例如:在禁用超线程技术的时候。

转载自并发编程网 – ifeve.com本文链接地址: Disruptor入门

通过工作池简单处理  WorkerPool:

        

 1 //Trade:交易实体类  
2
3         /**
4         *
5         *类描述:交易实例类
6         *@author: 豪
7         *@date: 日期:2017-5-17 时间:下午4:51:26
8         *@version 1.0
9         */
10           public class Trade {
11
12           private String id;//ID
13           private String name;
14           private double price;//金额
15           private AtomicInteger count = new AtomicInteger(0);
16
17           public String getId() {
18           return id;
19           }
20           public void setId(String id) {
21             this.id = id;
22             }
23           public String getName() {
24             return name;
25             }
26           public void setName(String name) {
27             this.name = name;
28             }
29           public double getPrice() {
30             return price;
31             }
32           public void setPrice(double price) {
33             this.price = price;
34             }
35           public AtomicInteger getCount() {
36             return count;
37             }
38           public void setCount(AtomicInteger count) {
39             this.count = count;
40             }
41           }
42
43   

     

 1 // TradeHandler:交易实体类 消费者处理器
2
3         
4
5        /**
6         *
7         *类描述:消费者管理器
8         *@author: 豪
9         *@date: 日期:2017-5-17 时间:下午5:56:12
10         *@version 1.0
11         */
12         public class TradeHandler implements WorkHandler<Trade> {
13
14         
15
16           @Override
17           public void onEvent(Trade event) throws Exception {
18           //这里做具体的消费逻辑
19           event.setId(UUID.randomUUID().toString());//简单生成下ID
20           System.out.println(event.getId());
21           }
22         }
23
24

     

 1  //main测试类:
2
3         
4
5         public class Main {
6           public static void main(String[] args) throws InterruptedException {
7           int BUFFER_SIZE=1024; //定义缓冲池大小 注意一定是2的N次方
8           int THREAD_NUMBERS=4; //定义线程池的大小
9
10           //创建一个消费类的Factory工厂
11           EventFactory<Trade> eventFactory = new EventFactory<Trade>() {
12             public Trade newInstance() {
13             return new Trade();
14             }
15            };
16
17         //创建一个缓冲池 参数包含消费者工厂 和指定缓冲池大小
18         RingBuffer<Trade> ringBuffer = RingBuffer.createSingleProducer(eventFactory, BUFFER_SIZE);
19
20         //创建SequenceBarrier 平衡方式
21         SequenceBarrier sequenceBarrier = ringBuffer.newBarrier();
22
23         //创建一个有界的线程池
24         ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUMBERS);
25
26         //实例化一个消费者 其必须实现 implements WorkHandler 的方法 。方法里面写具体的业务实现
27         WorkHandler<Trade> handler = new TradeHandler();
28
29
30
31         //创建一个工作池,把刚才实例化消费者的放在里面 参数包括缓冲池 sequenceBarrier平衡类 异常处理类 消费者
32         WorkerPool<Trade> workerPool = new WorkerPool<Trade>(ringBuffer, sequenceBarrier, new IgnoreExceptionHandler(), handler);
33
34         workerPool.start(executor);
35
36           //下面这个生产8个数据
37           for(int i=0;i<8;i++){
38           long seq=ringBuffer.next(); //获取到下一个空对象的位置下标
39           ringBuffer.get(seq).setPrice(Math.random()*9999); //设置缓冲池具体的消费数据
40           ringBuffer.publish(seq); //发布消费者
41           }
42
43         Thread.sleep(1000);
44         workerPool.halt(); //工作池停止
45         executor.shutdown(); //线程池关闭
46         }
47       }
48
49
50
51       

      

        

      

java架构《并发编程框架篇 __Disruptor》的更多相关文章

  1. java架构《并发线程高级篇四》

    本章主要讲并发线程的常见的两种锁.重入锁和读写锁 一:重入锁(ReentrantLock) 概念:重入锁,在需要进行同步的代码加锁,但最后一定不要忘记释放锁,否则会造成锁永远不能释放,其他线程进不了 ...

  2. java架构《并发线程高级篇一》

    本章主要记录讲解并发线程的线程池.java.util.concurrent工具包里面的工具类. 一:Executor框架: Executors创建线程池的方法: newFixedThreadPool( ...

  3. java架构《并发线程高级篇二》

    本章主要记录讲解并发线程的线程池.使用Executor框架自定义线程池. 自定义线程池使用Queue队列所表示出来的形式: 1 ArrayBlockingQueue<Runnable>(3 ...

  4. java架构《并发线程高级篇三》

    本章主要介绍和讲解concurrent.util里面的常用的工具类. 一.CountDownLatch使用:(用于阻塞主线程) 应用场景 :通知线程休眠和运行的工具类,是wait和notify的升级版 ...

  5. Java高并发 -- 线程池

    Java高并发 -- 线程池 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 在使用线程池后,创建线程变成了从线程池里获得空闲线程,关闭线程变成了将线程归坏给线程池. ...

  6. Java高并发--线程安全策略

    Java高并发--线程安全策略 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 不可变对象 发布不可变对象可保证线程安全. 实现不可变对象有哪些要注意的地方?比如JDK ...

  7. Java并发-线程池篇-附场景分析

    作者:汤圆 个人博客:javalover.cc 前言 前面我们在创建线程时,都是直接new Thread(): 这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,C ...

  8. Java高并发与多线程(四)-----锁

    今天,我们开始Java高并发与多线程的第四篇,锁. 之前的三篇,基本上都是在讲一些概念性和基础性的东西,东西有点零碎,但是像文科科目一样,记住就好了. 但是本篇是高并发里面真正的基石,需要大量的理解和 ...

  9. Java之创建线程的方式四:使用线程池

    import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.c ...

  10. java架构《并发线程中级篇》

    java多线程的三大设计模式 本章主要记录java常见的三大设计模式,Future.Master-Worker和生产者-消费者模式. 一.Future模式 使用场景:数据可以不及时返回,到下一次实际要 ...

随机推荐

  1. 初识分布式图数据库 Nebula Graph 2.0 Query Engine

    摘要:本文主要介绍 Query 层的整体结构,并通过一条 nGQL 语句来介绍其通过 Query 层的四个主要模块的流程. 一.概述 分布式图数据库 Nebula Graph 2.0 版本相比 1.0 ...

  2. notapai++ 使用小技巧

    alt+鼠标右键 建 实现整体添加字符 例: 25001510153394032 25001510153394034 25001510153393963 25001510153392080 25001 ...

  3. Redis缓存篇(四)缓存异常

    这一节,我们来学习一下缓存异常.缓存异常有四种类型,分别是缓存和数据库的数据不一致.缓存雪崩.缓存击穿和缓存穿透. 下面通过了解这四种缓存异常的原理和应对方法. 缓存和数据库的数据不一致 缓存和数据库 ...

  4. js--实现限制input输入框数字输入,实现每四位一个空格效果(银行卡号,手机号等)

    前言 工作学习中经常能遇到输入框限制输入数字,并且每四位一空格的情况,比如表单中银行卡号,手机号等输入框的限制,这里介绍一下使用js具体的实现方法.不需要引用第三方ui库. 正文 1.input标签的 ...

  5. [Java基础]——String类

    此篇博客主要整理Java中的String类的使用. 一.String    1.1  String 的定义 上图是jdk中对String类的定义,得到的信息有: ①.String类声明为final的, ...

  6. vue 侦听器watch 之 深度监听 deep

    <template> <div> <p>FullName: {{person.fullname}}</p> <p>FirstName: &l ...

  7. Docker学习笔记之基本命令使用

    测试的环境为Ubuntu1804. 1. search命令搜索镜像 sudo docker search centos 搜索centos相关的镜像,可以看到第一个最多星的的centos是官方的镜像,而 ...

  8. 跨站脚本漏洞(XSS)基础

    什么是跨站脚本攻击XSS 跨站脚本(cross site script),为了避免与样式css混淆所以简称为XSS,是一种经常出现在web应用中的计算机安全漏洞,也是web中最主流的攻击方式. 什么是 ...

  9. 【Linux】fdisk -l发现有加号"+"

    今天分区不够了,打算扩下分区,想起当时创建机器的时候还有大约80多个G没有用,今天打算重新利用上 就用了fdisk /dev/sda 创建了两个分区,但是发现分区下面有加号,感到而别的困惑 最后在很多 ...

  10. ctfhub技能树—web前置技能—http协议—响应包源代码

    打开靶机环境 查看网页是一个贪吃蛇小游戏 根据提示查看源码 发现flag 至此HTTP协议结束