Java的内置队列
队列 | 有界性 | 锁 | 数据结构 |
---|---|---|---|
ArrayBlockingQueue | bounded | 加锁 | arraylist |
LinkedBlockingQueue | optionally-bounded | 加锁 | linkedlist |
ConcurrentLinkedQueue | unbounded | 无锁(CAS) | linkedlist |
LinkedTransferQueue | unbounded | 无锁(CAS) | linkedlist |
PriorityBlockingQueue | unbounded | 加锁 | heap |
DelayQueue | unbounded | 加锁 | heap |
队列的底层一般分成三种:数组、链表和堆。其中,堆一般情况下是为了实现带有优先级特性的队列
Disruptor核心
- RingBuffer Disruptor
- Sequence SequenceBarrier
- WaitStrategy等待策略
- EventHandler消费者处理器
- WorkProcessor核心工作器
Disruptor
Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章种介绍了LMAX是一种 新型零售金融交易平台,它能够以很低的延迟产生大量交易,这个系统是建立在jvm平台上, 其核心是一个业务逻辑处理器:
- 它能够在一个线程里面每秒处理600万订单
- 业务逻辑处理器完全是运行在内存中,使用事件驱动方式
- 业务逻辑处理器的核心是Disruptor
Disruptor为什么高性能
- 数据结构:使用环形队列、数组、内存预加载
- 消除伪共享(填充缓存行)
- 使用单线程写方式,内存屏障(volatile变量)
- 序号栅栏和序号配合使用来消除锁和CAS
系统缓存优化-消除伪共享
- 缓存系统中是以缓存行(cache line)为单位存储的
- 缓存行是2的整数幂个连续字节,一般为32-256个字节
- 最常见的缓存行大小是64个字节
- Sequence
Disruptor 核心原理
- 一个环状队列,用在不同线程之间传递数据
- RingBuffer拥有一个序号,这个序号指向数组中下一个可用元素
- RingBuffer:基于数组的缓存实现,也是创建sequencer与定义WaitStrategy的入口
- Disruptor:持有RingBuffer、消费者线程池Executor、消费者集合ConsumerRepository等引用
Disruptor 核心-Sequence
- 通过顺序递增的序号来编号,管理进行交换的数据
- 对数据的处理过程总是沿着序号逐渐处理
- 一个Sequence用于跟踪标识某个特定的事件处理者(RingBuffer/Producer/Consumer)的处理进度, 多个Producer共用一个Sequence,但每一个Consumer分别对应一个Sequence, 当Producer获取的Sequence大于多个Consumer中的最小Sequence时,则等待,不再继续投递数据.
- Sequence可以看成是一个AtomicLong用于标识进度
- Sequence可以消除CPU缓存伪共享的问题
Disruptor 核心-Sequencer
- 实现类SingleProducerSequencer和MultiProducerSequencer
- 生产者和消费者之间快速,正确的传递数据的并发算法
Disruptor 核心-Sequencer Barrier
- 用于保持对RingBuffer的Main Published Sequence(Producer)和Consumer之间的平衡关系;
- Sequence Barrier用于判断决定Consumer是否还有可处理的事件.
Disruptor 核心-Event
- 从生产者到消费者过程中处理的数据
- Disruptor中没有代码表示Event,而是用户自定义的,其实就是对应的处理数据实体类
Disruptor 核心-EventProcessor
- 继承Runnable接口,处理Disruptor中的Event,拥有消费者的Sequence
- 其实现类BatchEventProcessor,包含了Event loop有效的实现,并且将回调到一个EventHandler接口实现
Disruptor 核心-EventHandler
- 代表一个消费者,用于处理队列中的数据,单消费者模式中使用
- 线程池数量必须等于EventHandler数量.
Disruptor 核心-WorkHandler
- 代表一个消费者,用于处理队列中的数据,多消费者模式中使用,
- 线程池数量必须等于WorkHandler数量.
- 参看我给官网的Issue: https://github.com/LMAX-Exchange/disruptor/issues/241
Disruptor 核心-WorkProcessor
确保每个Sequence只被一个processor消费,在同一个workPool中处理多个WorkProcessor不会消费同样的Sequence
Disruptor 核心-WaitStrategy
- 决定向队列添加数据时的等待策略
BlockingWaitStrategy
最低效的策略,但其对CPU的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现, 内部使用了ReentrantLock
SleepingWaitStrategy
性能表现跟BlockingWaitStrategy差不多,对CPU的消耗也类似,但其对生产者线程的影响最小, 适用于异步日志类的场景
YieldingWaitStrategy
性能是最好的,适用于低延时的系统. 在要求极高性能且事件处理线程数小于CPU逻辑核心数的场景中, 推荐使用此策略;例如,CPU开启超线程的特性.
该策略慎用: 内部采用Thread.yield();会让消费线程让出cpu,但又会在让出后同时竞争资源,导致CPU飙升.
disruptor.shutdown();
- 会等待所有的消费者消费完成后关闭,
- 但是不能在数据生产结束前调用shutdown,否则会导致过早的关闭disruptor,但是此时会继续投递数据, 而获取next Sequence时会导致死循环,因为(获取的生产者Sequence大于消费者Sequence,认为有数据没有消费完, 不能向该Sequence位置添加新数据,但是消费者此时已经被关闭,消费者Sequence不会再改变, 所以出现无限循环等待,无法继续向队列添加数据)
WorkerPool实现多消费者模式
在单消费者模式中,虽然一个Handler对应一个线程,但是这些Handler处理的是同一个数据,所以是单消费者模式.
多消费者模式: 多个线程处理的是不同的数据,队列中的数据只会被一条线程处理.
Disruptor中消费线程 串行操作(单消费者模式)
// 一个Handler对应一条线程,串行操作
disruptor.handleEventsWith(new Handler1())
.handleEventsWith(new Handler2())
.handleEventsWith(new Handler3());
Disruptor中消费线程 并行操作(单消费者模式)
// 一个Handler对应一条线程,并行操作
disruptor.handleEventsWith(new Handler1());
disruptor.handleEventsWith(new Handler2());
disruptor.handleEventsWith(new Handler3());
// 一个Handler对应一条线程,并行操作
disruptor.handleEventsWith(new Handler1(), new Handler2(), new Handler3());
Disruptor中消费线程 串并行混合操作(单消费者模式)
//Handler1 Handler2 并行执行完成后,串执行Handler3
disruptor.handleEventsWith(new Handler1(), new Handler2())
.handleEventsWith(new Handler3());
//Handler1 Handler2 并行执行完成后,串执行Handler3
EventHandlerGroup<Trade> group = disruptor.handleEventsWith(new Handler1(), new Handler2());
group.then(new Handler3());
6边形操作(单消费者模式)
--> h1 --> h2 -->
-- --
start --> --> h3
-- --
--> h4 --> h5 -->
h1和h4并行,h1和h2串行,h4和h5串行,h2和h5全部完成后执行 h3
Handler1 h1 = new Handler1();
Handler2 h2 = new Handler2();
Handler3 h3 = new Handler3();
Handler4 h4 = new Handler4();
Handler5 h5 = new Handler5();
disruptor.handleEventsWith(h1,h4);
disruptor.after(h1).handleEventsWith(h2);
disruptor.after(h4).handleEventsWith(h5);
disruptor.after(h2,h5).handleEventsWith(h3);
线程池设置参考规则
- 计算机密集型,耗cpu, 一般是cpu核心数+1或cpu核心数*2
- IO密集型,一般是 cpu核心数 / (1-0.9) 或者 cpu核心数 / (1-0.8) 例如:8核处理器, 8 / (1-0.9) = 80 条io线程
AOS架构
AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架.
- AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
- AQS定义两种资源共享方式: Exclusive, Share
isHeldExclusively 方法: 该线程是否正在独占资源
tryAcquire/tryRelease方法: 独占的方式尝试获取和释放资源
tryAcquireShared/tryReleaseShared方法: 共享方式
参考: https://www.jianshu.com/p/da9d051dcc3d
ReentrantLock 重入锁
- state初始值为0,表示未锁定状态
- A线程lock时,会调用tryAcquire()独占该锁并将 state + 1
- 其它线程再tryAcquire()时就会失败,直到A线程unlock()到state=0为止,其他线程才会有机会获取该锁
- A线程释放之前,A线程自己可以重复获取,此锁的(state会累加),这就是可重入的概念
- 但是要注意,获取多少次就要释放多少次,这样才能保证state是能回到0.
独享锁:该锁每一次只能被一个线程所持有。
共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
com.lmax.disruptor.SingleProducerSequencer.next(int) 源码分析
@Override
public long next(int n)
{
if (n < 1)
{
throw new IllegalArgumentException("n must be > 0");
}
long nextValue = this.nextValue;
long nextSequence = nextValue + n;
//wrapPoint用于判断当前的序号有没有绕过整个RingBuffer容器
//相当于标记生产者在队列中的逻辑队尾位置,
long wrapPoint = nextSequence - bufferSize;
//记录最小消费者序号
long cachedGatingSequence = this.cachedValue;
//如果生产者序号大于最小消费者序号,则可能需要等待
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
{
cursor.setVolatile(nextValue); // StoreLoad fence
//最小的序号
long minSequence;
//如果生产者序号大于消费者中最小的序号,则自旋,等待空间
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
{
LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
}
this.cachedValue = minSequence;
}
this.nextValue = nextSequence;
return nextSequence;
}