转载自:http://ifeve.com/disruptor-info/
用数组实现, 解决了链表节点分散, 不利于cache预读问题,可以预分配用于存储事件内容的内存空间;并且解决了节点每次需要分配和释放, 需要大量的垃圾回收GC问题 (数组内元素的内存地址的连续性存储的,在硬件级别,数组中的元素是会被预加载的,因为只要一个元素被加载到缓存行,其他相邻的几个元素也会被加载进同一个缓存行)
2.求余操作优化
求余操作本身也是一种高耗费的操作, 所以ringbuffer的size设成2的n次方, 可以利用位操作来高效实现求余。要找到数组中当前序号指向的元素,可以通过mod操作,正常通过sequence mod array length = array index,优化后可以通过:sequence & (array length-1) = array index实现。比如一共有8槽,3&(8-1)=3,HashMap就是用这个方式来定位数组元素的,这种方式比取模的速度更快。
3.预读与批量
相比链表队列,实现数组预读,减少结点操作空间释放和申请,从而减少gc次数。生产者支持单生产,多生产者模式,单生产者cursor使用普通long实现,无锁加快速度,多生产者才使用Sequence(AtomicLong)。
生产和消费元素支持单线程批量操作数据。
4.Lock-Free
系统态的锁会导致线程cache丢失. 锁竞争的时候需要进行仲裁. 这个仲裁会涉及到操作系统的内核切换, 并且在此过程中操作系统需要做一系列操作, 导致原有线程的指令缓存和数据缓很可能被丢掉。
用户态的锁往往是通过自旋锁来实现(自旋即忙等), 而自旋在竞争激烈的时候开销是很大的(一直在消耗CPU资源)
disruptor不使用锁, 使用CAS(Compare And Swap/Set),严格意义上说仍然是使用锁, 因为CAS本质上也是一种乐观锁, 只不过是CPU级别指令, 不涉及到操作系统, 所以效率很高(AtomicLong实现Sequence)
5.解决伪共享{False Sharing}
Cpu cache简单示意图:

上面谈到lock的耗费, 主要也是由于内核的切换导致cache的丢失。所以cache是优化的关键, cache越接近core就越快,也越小。
其中L1,L2,L3等级缓存都是由缓存行组成的, 通常是64字节, 一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量. 缓存行是缓存更新的基本单位, 就算你只读一个变量, 系统也会预读其余7个, 并cache这一行, 并且这行中的任一变量发生改变, 都需要重新加载整行, 而非仅仅重新加载一个变量.
伪共享举例:比如在链表中往往会连续定义head和tail指针, 所以对于cache-line的预读, 很有可能会导致head和tail在同一cache-line。在实际使用中, 往往producer线程会持续更改tail指针, 而consumer线程会持续更改head指针。
当producer线程和consumer线程分别被分配到core2和core1, 就会出现以下状况,由于core1不断改变head, 导致该cache-line过期, 对于core2, 虽然他不需要读head, 或者tail也没有改变, 但是由于cache-line的整行更新, 所以core2仍然需要不停的更新它的cache,导致cache效率底下,而实际情况下, core1会不断更新head, 而core2会不断更新tail, 导致core1和core2都需要频繁的重新load cache, 这就是伪共享问题。
在Disruptor里对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。
1.定义事件
SleepingWaitStrategy:和BlockingWaitStrategy一样,SpleepingWaitStrategy的CPU使用率也比较低。它的方式是循环等待并且在循环中间调用LockSupport.parkNanos(1)来睡眠,(在Linux系统上面睡眠时间60µs).然而,它的优点在于生产线程只需要计数,而不执行任何指令。并且没有条件变量的消耗。但是,事件对象从生产者到消费者传递的延迟变大了。SleepingWaitStrategy最好用在不需要低延迟,而且事件发布对于生产者的影响比较小的情况下。比如异步日志功能。
YieldingWaitStrategy:YieldingWaitStrategy是可以被用在低延迟系统中的两个策略之一,这种策略在减低系统延迟的同时也会增加CPU运算量。YieldingWaitStrategy策略会循环等待sequence增加到合适的值。循环中调用Thread.yield()允许其他准备好的线程执行。如果需要高性能而且事件消费者线程比逻辑内核少的时候,推荐使用YieldingWaitStrategy策略。例如:在开启超线程的时候。
BusySpinWaitStrategy:BusySpinWaitStrategy是性能最高的等待策略,同时也是对部署环境要求最高的策略。这个性能最好用在事件处理线程比物理内核数目还要小的时候。例如:在禁用超线程技术的时候。
一、Disruptor为什么快
1.数组实现用数组实现, 解决了链表节点分散, 不利于cache预读问题,可以预分配用于存储事件内容的内存空间;并且解决了节点每次需要分配和释放, 需要大量的垃圾回收GC问题 (数组内元素的内存地址的连续性存储的,在硬件级别,数组中的元素是会被预加载的,因为只要一个元素被加载到缓存行,其他相邻的几个元素也会被加载进同一个缓存行)
2.求余操作优化
求余操作本身也是一种高耗费的操作, 所以ringbuffer的size设成2的n次方, 可以利用位操作来高效实现求余。要找到数组中当前序号指向的元素,可以通过mod操作,正常通过sequence mod array length = array index,优化后可以通过:sequence & (array length-1) = array index实现。比如一共有8槽,3&(8-1)=3,HashMap就是用这个方式来定位数组元素的,这种方式比取模的速度更快。
3.预读与批量
相比链表队列,实现数组预读,减少结点操作空间释放和申请,从而减少gc次数。生产者支持单生产,多生产者模式,单生产者cursor使用普通long实现,无锁加快速度,多生产者才使用Sequence(AtomicLong)。
生产和消费元素支持单线程批量操作数据。
4.Lock-Free
系统态的锁会导致线程cache丢失. 锁竞争的时候需要进行仲裁. 这个仲裁会涉及到操作系统的内核切换, 并且在此过程中操作系统需要做一系列操作, 导致原有线程的指令缓存和数据缓很可能被丢掉。
用户态的锁往往是通过自旋锁来实现(自旋即忙等), 而自旋在竞争激烈的时候开销是很大的(一直在消耗CPU资源)
disruptor不使用锁, 使用CAS(Compare And Swap/Set),严格意义上说仍然是使用锁, 因为CAS本质上也是一种乐观锁, 只不过是CPU级别指令, 不涉及到操作系统, 所以效率很高(AtomicLong实现Sequence)
5.解决伪共享{False Sharing}
Cpu cache简单示意图:

上面谈到lock的耗费, 主要也是由于内核的切换导致cache的丢失。所以cache是优化的关键, cache越接近core就越快,也越小。
其中L1,L2,L3等级缓存都是由缓存行组成的, 通常是64字节, 一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量. 缓存行是缓存更新的基本单位, 就算你只读一个变量, 系统也会预读其余7个, 并cache这一行, 并且这行中的任一变量发生改变, 都需要重新加载整行, 而非仅仅重新加载一个变量.
伪共享举例:比如在链表中往往会连续定义head和tail指针, 所以对于cache-line的预读, 很有可能会导致head和tail在同一cache-line。在实际使用中, 往往producer线程会持续更改tail指针, 而consumer线程会持续更改head指针。
当producer线程和consumer线程分别被分配到core2和core1, 就会出现以下状况,由于core1不断改变head, 导致该cache-line过期, 对于core2, 虽然他不需要读head, 或者tail也没有改变, 但是由于cache-line的整行更新, 所以core2仍然需要不停的更新它的cache,导致cache效率底下,而实际情况下, core1会不断更新head, 而core2会不断更新tail, 导致core1和core2都需要频繁的重新load cache, 这就是伪共享问题。
在Disruptor里对RingBuffer的cursor和BatchEventProcessor的序列进行了缓存行填充。
二、示例
下面以车辆入场为例,入场后需要存入数据库,需要发送kafka消息,两步执行完后,给用户发送短信。代码实现如下:1.定义事件
public class InParkingDataEvent {
private String carLicense = "";
public void setCarLicense(String carLicense) {
this.carLicense = carLicense;
}
public String getCarLicense() {
return carLicense;
}
}
2.定义事件处理的具体实现
public class ParkingDataInDbHandler implements EventHandler<InParkingDataEvent>, WorkHandler<InParkingDataEvent> {
@Override
public void onEvent(InParkingDataEvent event) throws Exception {
long threadId = Thread.currentThread().getId();
String carLicense = event.getCarLicense();
System.out.println(String.format("Thread Id %s save %s into db ....", threadId, carLicense));
}
@Override
public void onEvent(InParkingDataEvent event, long sequence, boolean endOfBatch) throws Exception {
// TODO Auto-generated method stub
this.onEvent(event);
}
}
public class ParkingDataSmsHandler implements EventHandler<InParkingDataEvent> {
@Override
public void onEvent(InParkingDataEvent event, long sequence, boolean endOfBatch) throws Exception {
long threadId = Thread.currentThread().getId();
String carLicense = event.getCarLicense();
System.out.println(String.format("Thread Id %s send %s in plaza sms to user", threadId, carLicense));
}
}
public class ParkingDataToKafkaHandler implements EventHandler<InParkingDataEvent> {
@Override
public void onEvent(InParkingDataEvent event, long sequence,
boolean endOfBatch) throws Exception {
long threadId = Thread.currentThread().getId();
String carLicense = event.getCarLicense();
System.out.println(String.format("Thread Id %s send %s in plaza messsage to kafka...",threadId,carLicense));
}
}
3.发布事件类实现(Disruptor 要求 RingBuffer.publish 必须得到调用,如果发生异常也一样要调用 publish,那么,很显然这个时候需要调用者在事件处理的实现上来判断事件携带的数据是否是正确的或者完整的)
public class InParkingDataEventPublisher implements Runnable{
Disruptor<InParkingDataEvent> disruptor;
private CountDownLatch latch;
//private static int LOOP=10000;//模拟一万车辆入场
private static int LOOP=10;//模拟10车辆入场
public InParkingDataEventPublisher(CountDownLatch latch,Disruptor<InParkingDataEvent> disruptor) {
this.disruptor=disruptor;
this.latch=latch;
}
@Override
public void run() {
InParkingDataEventTranslator tradeTransloator=new InParkingDataEventTranslator();
for(int i=0;i<LOOP;i++){
disruptor.publishEvent(tradeTransloator);
try {
Thread.currentThread().sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countDown();
System.out.println("生产者写完" +LOOP + "个消息");
}
}
class InParkingDataEventTranslator implements EventTranslator<InParkingDataEvent> {
@Override
public void translateTo(InParkingDataEvent event, long sequence) {
this.generateTradeTransaction(event);
}
private InParkingDataEvent generateTradeTransaction(InParkingDataEvent event){
int num = (int)(Math.random()*8000);
num = num + 1000;
event.setCarLicense("京Z" + num);
System.out.println("Thread Id " + Thread.currentThread().getId() + " 写完一个event");
return event;
}
}
4.定义用于事件处理的线程池, 指定等待策略, 启动 Disruptor,执行完毕后关闭Disruptor
public static void main(String[] args) throws InterruptedException {
long beginTime = System.currentTimeMillis();
int bufferSize = 1024;
//Disruptor交给线程池来处理,共计 p1,c1,c2,c3四个线程
ExecutorService executor = Executors.newFixedThreadPool(4);
//构造缓冲区与事件生成
Disruptor<InParkingDataEvent> disruptor = new Disruptor<InParkingDataEvent>(new EventFactory<InParkingDataEvent>() {
@Override
public InParkingDataEvent newInstance() {
return new InParkingDataEvent();
}
}, bufferSize, executor, ProducerType.SINGLE, new YieldingWaitStrategy());
//使用disruptor创建消费者组C1,C2
EventHandlerGroup<InParkingDataEvent> handlerGroup = disruptor
.handleEventsWith(new ParkingDataToKafkaHandler(), new ParkingDataInDbHandler());
ParkingDataSmsHandler smsHandler = new ParkingDataSmsHandler();
//声明在C1,C2完事之后执行JMS消息发送操作 也就是流程走到C3
handlerGroup.then(smsHandler);
disruptor.start();//启动
CountDownLatch latch = new CountDownLatch(1);
//生产者准备
executor.submit(new InParkingDataEventPublisher(latch, disruptor));
latch.await();//等待生产者结束
disruptor.shutdown();
executor.shutdown();
System.out.println("总耗时:" + (System.currentTimeMillis() - beginTime));
}
三、 可选的等待策略
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是性能最高的等待策略,同时也是对部署环境要求最高的策略。这个性能最好用在事件处理线程比物理内核数目还要小的时候。例如:在禁用超线程技术的时候。