背景
Disruptor是英国外汇交易所LMAX开源的用于生产交易中的内存队列。
为了实现高性能交易撮合队列时,现在普遍的交易撮合引擎都采用了内存队列的方式,这种方式减少了持久化过程中带来的磁盘IO延迟,可以提交整体的交易性能。
Disruptor便是这样场景中诞生的,在实际使用过程中,LMAX基于Disruptor开发的系统单线程能支撑每秒600万订单。
Java队列
内存队列通常用于内存共享场景下的,而共享内存与锁有必然分不开的情缘,在java内置的线程安全的队列有以下:
队列 | 有界性 | 锁 | 数据结构 |
---|---|---|---|
ArrayBlockingQueue | bounded | 加锁 | arraylist |
LinkedBlockingQueue | optionally-bounded | 加锁 | linkedlist |
ConcurrentLinkedQueue | unbounded | 无锁 | linkedlist |
LinkedTransferQueue | unbounded | 无锁 | linkedlist |
PriorityBlockingQueue | unbounded | 加锁 | heap |
DelayQueue | unbounded | 加锁 | heap |
ConcurrentLinkedQueue以及LinkedTransferQueue的无锁都是通过原子变量compare and swap(以下简称“CAS”)操作的。
为什么要无锁
现实编程过程中,加锁通常会严重地影响性能。线程会因为竞争不到锁而被挂起,等锁被释放的时候,线程又会被恢复,这个过程中存在着很大的开销,并且通常会有较长时间的中断,因为当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,例如发生了缺页错误、调度延迟或者其它类似情况,那么所有需要这个锁的线程都无法执行下去。如果被阻塞线程的优先级较高,而持有锁的线程优先级较低,就会发生优先级反转。
Disruptor论文中讲述了一个实验:
这个测试程序调用了一个函数,该函数会对一个64位的计数器循环自增5亿次。
机器环境:2.4G 6核
运算: 64位的计数器累加5亿次
其测试结果:
Method | Time (ms) |
---|---|
Single thread | 300 |
Single thread with CAS | 5,700 |
Single thread with lock | 10,000 |
Single thread with volatile write | 4,700 |
Two threads with CAS | 30,000 |
Two threads with lock | 224,000 |
实际开发过的应该也知道这个结论:不加锁是最快的。
虽然知道无锁最快,但是我们核心还是要解决地是共享数据多线程安全访问呀(单线程能处理那么单线程最好了,不如actor模型,每个actor就是一个独占线程,反正永不停歇,那就没必要线程切换,保证效能)。
锁
下面是ArrayBlockingQueue通过加锁的方式实现的offer方法,保证线程安全。
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
insert(e);
return