Java并发编程高阶技术、高性能并发框架Disruptor

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Android_Mrchen/article/details/84581166

1.简介

Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易。这个系统是建立在JVM平台上,其核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单。业务逻辑处理器完全是运行在内存中,使用事件源驱动方式。业务逻辑处理器的核心是Disruptor。Disruptor它是一个开源的并发框架,并获得2011 Duke’s 程序框架创新奖,能够在无锁的情况下实现网络的Queue并发操作。Disruptor是一个高性能的异步处理框架,或者可以认为是最快的消息框架(轻量的JMS),也可以认为是一个观察者模式的实现,或者事件监听模式的实现。
开源:https://github.com/LMAX-Exchange/disruptor

2.使用

  1. 目前我们使用disruptor已经更新到了3.x版本,比之前的2.x版本性能更加的优秀,提供更多的API使用方式,直接下载jar包,addTopath就可以使用了。
  2. 也可以以maven的方式来引入
<!-- https://mvnrepository.com/artifact/com.lmax/disruptor -->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>
  1. 示例
    下面以一个例子来使用disruptor框架:
    先了解一下步骤:
    1. 建立一个Event类,用于创建Event类实例对象
 import java.text.SimpleDateFormat;
public class OrderEvent {

   private long value; //订单的价格

   public long getValue() {
   	return value;
   }

   public void setValue(long value) {
   	this.value = value;
   }
}
  1. 需要有一个监听事件类,用于处理数据(Event类)
  import java.text.SimpleDateFormat;
public class OrderEvent {

	private long value; //订单的价格

	public long getValue() {
		return value;
	}

	public void setValue(long value) {
		this.value = value;
	}
}
  1. 实例化Disruptor实例,配置参数,编写Disruptor
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;

public class Main {

	
	
	public static void main(String[] args) {
		
		
		// 参数准备工作
		OrderEventFactory orderEventFactory = new OrderEventFactory();
		int ringBufferSize = 1024*1024;
		ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
		
		/**
		 * 建立OrderEvent类
		 * 1 eventFactory: 消息(event)工厂对象  实现   implements EventFactory<OrderEvent>接口
		 * OrderEventHandler implements EventHandler<OrderEvent>  用来接受数据
		 * 2 ringBufferSize: 容器的长度
		 * 3 executor: 线程池(建议使用自定义线程池) RejectedExecutionHandler
		 * 4 ProducerType: 单生产者 还是 多生产者
		 * 5 waitStrategy: 等待策略
		 */
		//1. 实例化disruptor对象
		Disruptor<OrderEvent> disruptor = new Disruptor<OrderEvent>(orderEventFactory,
				ringBufferSize,   //容器大小
				executor,          //线程池
				ProducerType.SINGLE,   //SINGLE\MULTI 单生产者模式和 多生产者模式       
				new BlockingWaitStrategy());   //等待策略  阻塞策略
		
		//2. 添加消费者的监听 (构建disruptor 与 消费者的一个关联关系)
		disruptor.handleEventsWith(new OrderEventHandler());
		
		//3. 启动disruptor
		disruptor.start();
		
		//4. 获取实际存储数据的容器: RingBuffer
		RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
		
		
		//订单生产者
		OrderEventProducer producer = new OrderEventProducer(ringBuffer);
		
		ByteBuffer bb = ByteBuffer.allocate(8);
		
		for(long i = 0 ; i < 100; i ++){
			bb.putLong(0, i);
			producer.sendData(bb);
		}
		
		disruptor.shutdown();
		executor.shutdown();
		
	}
}

  1. 编写生产者组件,向Disruptor容器中去投递数据
import java.nio.ByteBuffer;

import com.lmax.disruptor.RingBuffer;

public class OrderEventProducer {

	private RingBuffer<OrderEvent> ringBuffer;
	
	public OrderEventProducer(RingBuffer<OrderEvent> ringBuffer) {
		this.ringBuffer = ringBuffer;
	}
	
	public void sendData(ByteBuffer data) {
		//1 在生产者发送消息的时候, 首先 需要从我们的ringBuffer里面 获取一个可用的序号
		long sequence = ringBuffer.next();	//0	
		try {
			//2 根据这个序号, 找到具体的 "OrderEvent" 元素 注意:此时获取的OrderEvent对象是一个没有被赋值的"空对象"
			OrderEvent event = ringBuffer.get(sequence);
			//3 进行实际的赋值处理
			event.setValue(data.getLong(0));			
		} finally {
			//4 提交发布操作
			ringBuffer.publish(sequence);			
		}
	}

	
}

3.Disruptor为什么这么快

  1. 环形队列RingBuffer
    一个环形队列,意味着首尾相连,对列可以循环使用,使用数组来保存。环形队列在JVM生命周期中通常是永生的,GC的压力更小。
    在这里插入图片描述
    我们来解释一下这个图:当前有一个consumer,停留在位置12,这时producer假设在位置3,这时producer的下一步是如何处理的呢?producer会尝试读取4,发现下一个可以获取,所以可以安全获取,并且通知一个阻塞的consumer起来活动。如此一直到下一圈11都是安全的(这里我们假设生产者比较快),当producer尝试访问12时发现不能继续,于是自旋等待;当consumer消费时,会调用barrier的waitFor方法,waitFor看到前面最近的安全节点已经到了下一圈的11,于是consumer可以无锁的去消费当前12到下一圈11所有数据,可以想象,这种方式比起synchronized要快上很多倍。
  2. 弃用锁机制使用CAS
    在高度竞争的情况下,锁的性能将超过原子变量的性能,但是更真实的竞争情况下,原子变量的性能将超过锁的性能。同时原子变量不会有死锁等活跃性问题。能不用锁,就不使用锁,如果使用,也要将锁的粒度最小化。

唯一使用锁的就是消费者的等待策略实现类中,下图。补充一句,生产者的等到策略就是LockSupport.parkNanos(1),再自旋判断。

  1. 等待策略
名称 措施 适用场景
BlockingWaitStrategy 加锁 CPU资源紧缺,吞吐量和延迟并不重要的场景
BusySpinWaitStrategy 自旋 通过不断重试,减少切换线程导致的系统调用,而降低延迟。推荐在线程绑定到固定的CPU的场景下使用
PhasedBackoffWaitStrategy 自旋 + yield + 自定义策略 策略 CPU资源紧缺,吞吐量和延迟并不重要的场景
SleepingWaitStrategy 自旋 + yield + sleep 性能和CPU资源之间有很好的折中。延迟不均匀
TimeoutBlockingWaitStrategy 加锁,有超时限制 CPU资源紧缺,吞吐量和延迟并不重要的场景
YieldingWaitStrategy 自旋 + yield + 自旋 性能和CPU资源之间有很好的折中。延迟比较均匀
  1. 解决伪共享,采用缓存行填充
    在这里插入图片描述
    从上图看到,线程1在CPU核心1上读写变量X,同时线程2在CPU核心2上读写变量Y,不幸的是变量X和变量Y在同一个缓存行上,每一个线程为了对缓存行进行读写,都要竞争并获得缓存行的读写权限,如果线程2在CPU核心2上获得了对缓存行进行读写的权限,那么线程1必须刷新它的缓存后才能在核心1上获得读写权限,这导致这个缓存行在不同的线程间多次通过L3缓存来交换最新的拷贝数据,这极大的影响了多核心CPU的性能。
    下面代码解决伪共享问题的,就是实例变量前后各加7个long形变量,用空间换时间。
abstract class SingleProducerSequencerPad extends AbstractSequencer
{
    protected long p1, p2, p3, p4, p5, p6, p7;

    SingleProducerSequencerPad(int bufferSize, WaitStrategy waitStrategy)
    {
        super(bufferSize, waitStrategy);
    }
}

public final class SingleProducerSequencer extends SingleProducerSequencerFields
{
    protected long p1, p2, p3, p4, p5, p6, p7;
    //..省略
}

Java中通过填充缓存行,来解决伪共享问题的思路,现在可能已经是老生常谈,连Java8中都新增了sun.misc.Contended注解来避免伪共享问题。但在Disruptor刚出道那会儿,用缓存行来优化Java数据结构,这恐怕还很新潮。

  1. 还有一些细节性的
    1)通过sequence & (bufferSize - 1)定位元素的index比普通的求余取模(%)要快得多。sequence >>> indexShift 快速计算出sequence/bufferSize的商flag(其实相当于当前sequence在环形跑道上跑了几圈,在数据生产时要设置好flag。

    2)合理使用Unsafe,CPU级别指令。实现更加高效地内存管理和原子访问。
    至于一些更细节的,下面源码搞起来,还是很简单的。

3. 高性能之道-内核-使用单线程写-系统级别内存屏障实现-消除伪共享-序号栅栏机制

  1. disruptor的RingBuffer,之所以可以做到完全无锁,也是因为“单线程写”,这是所有“前提的前提”。
  2. 离开了这个前提条件,没有任何技术可以做到完全无锁。
  3. redis、netty(串行,并行有多线程竞争问题,buffer池)等等高性能技术框架的设计都是这个核心思想。
  4. 要正确的实现无锁,还需要另外一个关键技术:内存屏障
  5. 对应到java语言,就是valotile变量与happens before 语义。
  6. 内存屏障 -linux的smp_wmb()/smp_rmb()。
  7. 缓存系统中以缓存行为单位存储的
  8. 缓存行是2的整数幂个连续字节,一般为32-256个字节
  9. 最常见的缓存行大小是64个字节
  10. **当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行 **
  11. **就会无意中影响批次的性能,这就是伪共享。(左边填充七个long缓存行、右边填充7个long缓存行,以空间换时间实现伪共享,减少资源竞争) **
  12. disruptor3.0中,序号栅栏 sequenceBarrier和序号sequence搭配使用
  13. 协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用
  14. 消费者序号小于生产者序号数值
  15. 消费者序号数值必须小于前置(依赖关系)消费者的序号数值
  16. 生产者号数值不能大于消费者中最小的序号数值
  17. 以避免生产者速度过快,将还未来及消费的消息覆盖。
@Override
    public long next(int n)   //1
    {
        if (n < 1)     //初始值 sequence= -1
        {
            throw new IllegalArgumentException("n must be > 0");
        }

        long nextValue = this.nextValue;   //语义级别的: nextvalue 为singleProducerSequencer的变量

        long nextSequence = nextValue + n;   //0
        long wrapPoint = nextSequence - bufferSize;  //  -10 wrapPoint用于判断当前的序号有没有绕过整个ringbuffer容器
        //这个cachedGatingSequence 他的目的就是不用每次都去进行获取消费者的最小序号,用一个缓存区进行接收
        long cachedGatingSequence = this.cachedValue;   //-1   cachedValue我不太清楚   但是语义上说是进行缓存优化的 

        if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
        {
            long minSequence;  //最小序号
          // 如果你的生产者序号大于消费者中最小的序号  那么你就挂起 进行自旋操作
          //生产者号数值不能大于消费者中最小的序号数值
            while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))   //自旋操作  自旋锁     避免加锁  与cas操作
            // Util.getMinimumSequence(gatingSequences, nextValue)  找到消费者中最小的序号值
            {
                LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
            }

            this.cachedValue = minSequence;
        }

        this.nextValue = nextSequence;

        return nextSequence;
    }

4. Disruptor源码解读

  1. 核心类接口
    Disruptor 提供了对RingBuffer的封装。
    RingBuffer 环形队列,基于数组实现,内存被循环使用,减少了内存分配、回收扩容等操作。
    EventProcessor 事件处理器,实现了Runnable,单线程批量处理BatchEventProcessor和多线程处理WorkProcessor。
    Sequencer 生产者访问序列的接口,RingBuffer生产者的父接口,其直接实现有SingleProducerSequencer和MultiProducerSequencer。
    EventSequencer 空接口,暂时没用,用于以后扩展。
    SequenceBarrier 消费者屏障 消费者用于访问缓存的控制器。
    WaitStrategy 当没有可消费的事件时,根据特定的实现进行等待。
    SingleProducerSequencer 单生产者发布实现类
    MultiProducerSequencer 多生产者发布实现类

参考:
https://www.cnblogs.com/lewis09/p/9974995.html

猜你喜欢

转载自blog.csdn.net/Android_Mrchen/article/details/84581166