java架构《并发编程框架篇 __Disruptor》
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》的更多相关文章
- java架构《并发线程高级篇四》
本章主要讲并发线程的常见的两种锁.重入锁和读写锁 一:重入锁(ReentrantLock) 概念:重入锁,在需要进行同步的代码加锁,但最后一定不要忘记释放锁,否则会造成锁永远不能释放,其他线程进不了 ...
- java架构《并发线程高级篇一》
本章主要记录讲解并发线程的线程池.java.util.concurrent工具包里面的工具类. 一:Executor框架: Executors创建线程池的方法: newFixedThreadPool( ...
- java架构《并发线程高级篇二》
本章主要记录讲解并发线程的线程池.使用Executor框架自定义线程池. 自定义线程池使用Queue队列所表示出来的形式: 1 ArrayBlockingQueue<Runnable>(3 ...
- java架构《并发线程高级篇三》
本章主要介绍和讲解concurrent.util里面的常用的工具类. 一.CountDownLatch使用:(用于阻塞主线程) 应用场景 :通知线程休眠和运行的工具类,是wait和notify的升级版 ...
- Java高并发 -- 线程池
Java高并发 -- 线程池 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 在使用线程池后,创建线程变成了从线程池里获得空闲线程,关闭线程变成了将线程归坏给线程池. ...
- Java高并发--线程安全策略
Java高并发--线程安全策略 主要是学习慕课网实战视频<Java并发编程入门与高并发面试>的笔记 不可变对象 发布不可变对象可保证线程安全. 实现不可变对象有哪些要注意的地方?比如JDK ...
- Java并发-线程池篇-附场景分析
作者:汤圆 个人博客:javalover.cc 前言 前面我们在创建线程时,都是直接new Thread(): 这样短期来看是没有问题的,但是一旦业务量增长,线程数过多,就有可能导致内存异常OOM,C ...
- Java高并发与多线程(四)-----锁
今天,我们开始Java高并发与多线程的第四篇,锁. 之前的三篇,基本上都是在讲一些概念性和基础性的东西,东西有点零碎,但是像文科科目一样,记住就好了. 但是本篇是高并发里面真正的基石,需要大量的理解和 ...
- Java之创建线程的方式四:使用线程池
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.c ...
- java架构《并发线程中级篇》
java多线程的三大设计模式 本章主要记录java常见的三大设计模式,Future.Master-Worker和生产者-消费者模式. 一.Future模式 使用场景:数据可以不及时返回,到下一次实际要 ...
随机推荐
- JavaScript中对象是否需要加引号?
对象的属性名是包括空字符串在内的所有字符串. 那么问题来了,我们平时定义的对象如下,是没有引号""or''的,这样不加引号有没有错呢? 答案是,加不加分情况!但加了肯定没问题... ...
- 使用mono-repo实现跨项目组件共享
本文会分享一个我在实际工作中遇到的案例,从最开始的需求分析到项目搭建,以及最后落地的架构的整个过程.最终实现的效果是使用mono-repo实现了跨项目的组件共享.在本文中你可以看到: 从接到需求到深入 ...
- 实战Git命令(界面操作+命令行)
先说明下公司的发版步骤,当需要开发一个新的功能,先从master分支中拉出一个自己的分支a(假设分支为a),在a分支开发功能完后,需要切换到dev分支,然后把自己的分支a合到dev分支,部署测试环境让 ...
- Java springboot支付宝小程序授权,获取用户信息,支付及回调
参考官方文档https://opendocs.alipay.com/mini/introduce/pay 支付宝小程序的支付和微信小程序的支付一样第一步都是要获取到用户的唯一标识,在微信中我们获取到的 ...
- Mac M1原生(ARM64)Golang dev&debug
前言 通过本文最终实现了在M1芯片的Mac mini上的Goland的开发,并通过编译源码解决了无法DEBUG的问题. Go 1.16版将正式支持Apple Silicon M1芯片,即arm64架构 ...
- 【Java基础】Java8 新特性
Java8 新特性 Lambda 表达式 Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递).使用它可以写出更简洁.更灵活的代码. L ...
- 【C++】《Effective C++》第八章
第八章 定制new和delete 对于程序开发来说,了解C++内存管理例程的行为是非常重要的.其中两个主角是分配例程和归还例程(operator new和operator delete),配角是new ...
- Vue基础语法与指令
项目初始化 用vscode打开终端,输入npm init -y生成package.json 然后安装vue npm install vue 需要注意的是,我遇到了这个问题 出现原因:文件夹名和生成的p ...
- QA职责
- Java反射全解析(使用、原理、问题、在Android中的应用)
前言 今天说Java模块内容:反射. 反射介绍 正常情况下,我们知晓我们要操作的类和对象是什么,可以直接操作这些对象中的变量和方法,比如一个User类: User user=new User(); u ...