本文译自Dirsruptor在github上的wiki中文章:Getting Started

获取Disruptor

Disruptor jar包可以从maven仓库mvnrepository获取,可以将其集成进项目的依赖管理中。

<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>

### 编写事件处理生产者和消费者

为了学习Disruptor的使用,这里以非常简单的例子入手:生产者生产单个long型value传递给消费者。这里简化消费者逻辑,只打印消费的value。首先定义携带数据的Event:

public class LongEvent
{
private long value; public void set(long value)
{
this.value = value;
}
}

为了允许Disruptor能够为我们预分配这些事件,我们需要一个EventFactory用于构造事件:

public class LongEventFactory implements EventFactory<LongEvent>
{
public LongEvent newInstance()
{
return new LongEvent();
}
}

一旦我们定义了事件,我便再需要创建事件消费者用于消费处理事件。在我们的例子中,我们只需要打印value值到控制台即可:

public class LongEventHandler implements EventHandler<LongEvent>
{
public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
{
System.out.println("Event: " + event);
}
}

有了事件消费者,我们还需要事件生产者产生事件。为了简单起见,我们假设数据来源于I/O,如:网络或者文件。由于不同版本的Disruptor,提供了不同的方式编写生产者。

随着3.0版本,Disruptor通过将复杂逻辑囊括在RingBuffer中,从而提供了丰富的Lambda-style API帮助开发者构建Producer。因此从3.0之后,更偏爱使用Event Publisher/Event Translator的API发布消息:

public class LongEventProducerWithTranslator
{
private final RingBuffer<LongEvent> ringBuffer; public LongEventProducerWithTranslator(RingBuffer<LongEvent> ringBuffer)
{
this.ringBuffer = ringBuffer;
} private static final EventTranslatorOneArg<LongEvent, ByteBuffer> TRANSLATOR =
new EventTranslatorOneArg<LongEvent, ByteBuffer>()
{
public void translateTo(LongEvent event, long sequence, ByteBuffer bb)
{
event.set(bb.getLong(0));
}
}; public void onData(ByteBuffer bb)
{
ringBuffer.publishEvent(TRANSLATOR, bb);
}
}

这种方式的另一个优势在于Translator代码可以被分离在单独的类中,同时也比较容易进行无依赖的单元测试。Disruptor提供了许多不同的接口(EventTranslator, EventTranslatorOneArg, EventTranslatorTwoArg, etc.),可以通过实现这些接口提供translators。原因是允许转换器被表示为静态类或非捕获lambda作为转换方法的参数通过Ring Buffer上的调用传递给转换器。

另一方式使用3.0版本之前的遗留API构建生产者发布消息,这种方式比较原始:

public class LongEventProducer
{
private final RingBuffer<LongEvent> ringBuffer; public LongEventProducer(RingBuffer<LongEvent> ringBuffer)
{
this.ringBuffer = ringBuffer;
} public void onData(ByteBuffer bb)
{
long sequence = ringBuffer.next(); // Grab the next sequence
try
{
LongEvent event = ringBuffer.get(sequence); // Get the entry in the Disruptor
// for the sequence
event.set(bb.getLong(0)); // Fill with data
}
finally
{
ringBuffer.publish(sequence);
}
}
}

从以上的代码流程编写可以看出,事件的发布比使用一个简单的队列要复杂。这是由于需要对事件预分配导致。对于消息的发布有两个阶段,首先在RingBuffer中声明需要的槽位,然后再发布可用的数据。必须使用try/finally语句块包裹消息的发布。必须现在try块中声明使用RingBuffer的槽位,然后再finally块中发布使用的sequece。如果不这样做,将可能导致Disruptor状态的错误,特别是在多生产者的情况下,如果不重启Disruptor将不能恢复。因此推荐使用EventTranslator编写producer。

最后一步需要将以上编写的组件连接起来。虽然可以手动连接各个组件,然而那样可能比较复杂,因此提供了一个DSL用于构造以便简化过程。使用DSL带来装配的简化,但是却对于很多参数无法做到更细致的控制,然而对于大多数情况,DSL还是非常适合:

public class LongEventMain
{
public static void main(String[] args) throws Exception
{
// 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<>(factory, bufferSize, DaemonThreadFactory.INSTANCE); // 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);
}
}
}

关于对Disruptor的接口设计的影响之一是Java 8,因为它使用了Functional Interfaces去实现Java Lambdas。在Disruptor API的大多数接口都被定义成Functional Interfaces以便Lambdas可以被使用。以上的LongEventMain可以使用Lambdas进行简化:

public class LongEventMain
{
public static void main(String[] args) throws Exception
{
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024; // Construct the Disruptor
Disruptor<LongEvent> disruptor = new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE); // Connect the handler
disruptor.handleEventsWith((event, sequence, endOfBatch) -> System.out.println("Event: " + event)); // 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(); ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
Thread.sleep(1000);
}
}
}

可以看出使用Lambdas有大量的类将不再需要,如:handler,translator等。也可以看出使用Lambdas简化publishEvent()只仅仅涉及到参数传递。

然而如果将代码改成这样:

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

注意这里使用了捕获式的Lambda,意味着通过调用publishEvent()时可能需要实例化一个对象来持有ByteBuffer bb将其传递给lambda。这个将可能创建额外的垃圾,如果对GC压力有严格要求的情况下,通过传递参数的方式将更加受欢迎。

使用方法引用来代理上述的lambda将能进一步简化上述的方式,也将更时髦:

public class LongEventMain
{
public static void handleEvent(LongEvent event, long sequence, boolean endOfBatch)
{
System.out.println(event);
} public static void translate(LongEvent event, long sequence, ByteBuffer buffer)
{
event.set(buffer.getLong(0));
} public static void main(String[] args) throws Exception
{
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024; // Construct the Disruptor
Disruptor<LongEvent> disruptor = new Disruptor<>(LongEvent::new, bufferSize, DaemonThreadFactory.INSTANCE); // Connect the handler
disruptor.handleEventsWith(LongEventMain::handleEvent); // 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(); ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
ringBuffer.publishEvent(LongEventMain::translate, bb);
Thread.sleep(1000);
}
}
}

这里对ringBuffer.publishEvent的参数使用了方法引用替换了lambda,使其更进一步简化。

### 基本的参数设置

对于大多数场景使用方式即可。然而,如果你能确定硬件和软件的环境便可以进一步对Disruptor的参数进行调整以提高性能。主要有两种参数可以被调整:

  • single vs. multiple producers
  • alternative wait strategies

Single vs. Multiple Producers

提高并发系统的性能的最好方式是遵循Single Writer Principle,这个也在Disruptor也被应用。如果在你的场景中只仅仅是单生产者,然后你可以调优获得额外的性能提升:

public class LongEventMain
{
public static void main(String[] args) throws Exception
{
//.....
// Construct the Disruptor with a SingleProducerSequencer
Disruptor<LongEvent> disruptor = new Disruptor(
factory, bufferSize, ProducerType.SINGLE, new BlockingWaitStrategy(), DaemonThreadFactory.INSTANCE);
//.....
}
}

为了说明通过这种技术方式能替身多少性能优势,这里有一份测试类OneToOne performance test。在i7 Sandy Bridge MacBook Air的运行结果:

Multiple Producer:

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

Single Producer:

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

Alternative Wait Strategies

Disruptor默认使用的等待策略是BlockingWaitStrategy。内部的BlockingWaitStrategy使用典型的Lock和Condition处理线程的wake-up。BlockingWaitStrategy是等待策略中最慢的,但是在CPU使用率方面是最保守的,最广泛的适用于大多数场景。可以通过调整等待策略参数获取额外的性能。

1.SleepingWaitStrategy

类似BlockingWaitStrategy,SleepingWaitStrategy也试图保持CPU使用率。通过使用简单的忙等循环,但是在循环过程中调用了LockSupport.parkNanos(1)。在典型的Linux系统上停顿线程60us。然而,它具有以下好处:生产线程不需要采取任何其他增加适当计数器的动作,并且不需要发信号通知条件变量的成本。然而将增大生产者和消费者之前数据传递的延迟。在低延迟没有被要求的场景中,这是一个非常好的策略。一个公共的使用场景是异步日志。

2.YieldingWaitStrategy

YieldingWaitStrategy是一个低延迟系统中等待策略。通过牺牲CPU资源来降低延迟。YieldingWaitStrategy通过busy spin等待sequence增长到合适的值。在内部实现中,通过在循环内部使用Thread.yield()允许其他的队列线程运行。当需要很高的性能且事件处理线程少于CPU逻辑核数时这个策略被强烈推荐。如:启用了超线程。

3.BusySpinWaitStrategy

BusySpinWaitStrategy是高新跟那个的等待策略,但是对环境有限制。如果事件处理器的数量小于物理核数时才使用这个策略。

### 清理RingBuffer中的对象

当通过Disruptor传递数据时,对象的存活时间可能超过预期。为了能够避免这个发生,在事件处理结束后应当清理下事件对象。如果只有单个生产者,在该生产者中清理对象即是最高效的。然后有时间处理链时,就需要特定的事件处理器被放置在链的最末尾用于清理事件。

class ObjectEvent<T>
{
T val; void clear()
{
val = null;
}
} public class ClearingEventHandler<T> implements EventHandler<ObjectEvent<T>>
{
public void onEvent(ObjectEvent<T> event, long sequence, boolean endOfBatch)
{
// Failing to call clear here will result in the
// object associated with the event to live until
// it is overwritten once the ring buffer has wrapped
// around to the beginning.
event.clear();
}
} public static void main(String[] args)
{
Disruptor<ObjectEvent<String>> disruptor = new Disruptor<>(
() -> ObjectEvent<String>(), bufferSize, DaemonThreadFactory.INSTANCE); disruptor
.handleEventsWith(new ProcessingEventHandler())
.then(new ClearingObjectHandler());
}

Disruptor系列(二)— disruptor使用的更多相关文章

  1. Disruptor 系列(二)使用场景

    Disruptor 系列(二)使用场景 今天用一个订单问题来加深对 Disruptor 的理解.当系统中有订单产生时,系统首先会记录订单信息.同时也会发送消息到其他系统处理相关业务,最后才是订单的处理 ...

  2. Disruptor 系列(一)快速入门

    Disruptor 系列(一)快速入门 Disruptor:是一个开源的并发框架,能够在 无锁 的情况下实现网络的 Queue 并发操作,所以处理数据的能力比 Java 本身提供的并发类容器要大的多, ...

  3. 高性能队列Disruptor系列2--浅析Disruptor

    1. Disruptor简单介绍 Disruptor是一个由LMAX开源的Java并发框架.LMAX是一种新型零售金融交易平台,这个系统是建立在 JVM 平台上,核心是一个业务逻辑处理器,它能够在一个 ...

  4. 高性能队列Disruptor系列3--Disruptor的简单使用(译)

    简单用法 下面以一个简单的例子来看看Disruptor的用法:生产者发送一个long型的消息,消费者接收消息并打印出来. 首先,我们定义一个Event: public class LongEvent ...

  5. 前端构建大法 Gulp 系列 (二):为什么选择gulp

    系列目录 前端构建大法 Gulp 系列 (一):为什么需要前端构建 前端构建大法 Gulp 系列 (二):为什么选择gulp 前端构建大法 Gulp 系列 (三):gulp的4个API 让你成为gul ...

  6. WPF入门教程系列二十三——DataGrid示例(三)

    DataGrid的选择模式 默认情况下,DataGrid 的选择模式为“全行选择”,并且可以同时选择多行(如下图所示),我们可以通过SelectionMode 和SelectionUnit 属性来修改 ...

  7. Web 开发人员和设计师必读文章推荐【系列二十九】

    <Web 前端开发精华文章推荐>2014年第8期(总第29期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  8. Web 前端开发人员和设计师必读文章推荐【系列二十八】

    <Web 前端开发精华文章推荐>2014年第7期(总第28期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

  9. Web 开发精华文章集锦(jQuery、HTML5、CSS3)【系列二十七】

    <Web 前端开发精华文章推荐>2014年第6期(总第27期)和大家见面了.梦想天空博客关注 前端开发 技术,分享各类能够提升网站用户体验的优秀 jQuery 插件,展示前沿的 HTML5 ...

随机推荐

  1. verilog常见错误列表

    Error/Warning 来源:https://hdlbits.01xz.net/wiki/ 题目: 1.Quartus Warning 10235: Warning (): Verilog HDL ...

  2. fprintf()函数

    fprintf函数可以将数据按指定格式写入到文本文件中.其调用格式为: 数据的格式化输出:fprintf(fid,format,variables) 按指定的格式将变量的值输出到屏幕或指定文件 fid ...

  3. Spring Cloud Alibaba 实战(十二) - Nacos配置管理

    本章主要内容是:使用Nacos管理配置以及实现配置管理的原因,配置如何管理以及动态刷新和最佳实现总结,最后是Nacos配置刷新原理解读 该技术类似于Spring Cloud Config 1 配置管理 ...

  4. Flutter竟然发布了1.5版本!!!!

    2018年2月,Flutter推出了第一个Beta版本,在2018年12月5日,Flutter1.0版本发布,当时用了用觉得这个东西非常好用,对于当时被RN搞的头皮发麻的我来说简直是看到了曙光.而在昨 ...

  5. Java题库——chapter7 多维数组

    1)Which of the following statements are correct? 1) _______ A)char[ ][ ] charArray = {{'a', 'b'}, {' ...

  6. SpringCloud的入门学习之概念理解、Eureka服务注册与发现入门

    1.微服务与微服务架构.微服务概念如下所示: 答:微服务强调的是服务的大小,它关注的是某一个点,是具体解决某一个问题.提供落地对应服务的一个服务应用,狭意的看,可以看作Eclipse里面的一个个微服务 ...

  7. extjs 动态加载列表,优化思路

    功能截图 之前做法,先查询每一行的前4个字段,然后动态拼接出其他的字段,效率极低,以下是优化后的代码,供参考,只提供一个优化思路,授人以鱼不如授人以渔 后台Sql语句优化(语法仅支持Oracle) S ...

  8. Supply Initial Data提供初始数据 (EF)

    Open the Updater.cs (Updater.vb) file, located in the MySolution.Module project's Database Update fo ...

  9. Microsoft Visual Studio 2017 找不到 Visual Studio Installer

    Microsoft Visual Studio 2017 找不到 Visual Studio Installer ? 打开vs2017 ,选择 工具 --> 扩展和更新 --> 联机,搜索 ...

  10. arcgis api 4.x for js 集成 Echarts4 实现模拟迁徙图效果(附源码下载)

    前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 4.x for js:esri 官网 api,里面详细的介绍 arcgis api 4.x 各个类 ...