作者|海带
编辑|包包
几年前听说过Disruptor,一直没用过也没深究, 其号称是一个性能爆表的并发队列,上Github/LMAX-Exchange/disruptor 去看了看,官方性能描述文章 选了慢如蜗牛的ArrayBlockQueue来对比。在Nehalem 2.8Ghz – Windows 7 SP1 64-bit录得性能见后(其中P,C分别代表 Producer和Consumer):
1P – 1C 的吞吐量两千五百万次,1P – 3C Multicast 就降到了一千万次不到,对比我所认为的非线程安全1P -1C队列亿次每秒的量级,感觉并不强大。亿次每秒的队列加上线程安全,毛估估1P-1C性能减半五千万次每秒,1P-3C 再减少个30%三千五百万次每秒,应该差不多了吧。
继续读读Disruptor的介绍,整体业务框架先不谈,关于队列的部分,我只想问可以说脏话不,太TMD的复杂了,实现方式一点都不优雅,讲真,我想用100行代码灭了它。
说干就干,先尝试一下。
– | ArrayBlockingQueue | Disruptor |
---|---|---|
Unicast: 1P – 1C | 5,339,256 | 25,998,336 |
Pipeline: 1P – 3C | 2,128,918 | 16,806,157 |
Sequencer: 3P – 1C | 5,539,531 | 13,403,268 |
Multicast: 1P – 3C | 1,077,384 | 9,377,871 |
Diamond: 1P – 3C | 2,113,941 | 16,143,613 |
Comparative throughput (in ops per sec)
第一步 简单粗暴的阻塞队列
先简单来个,这年头用synchronized要被人瞧不起的(风闻现在性能好多了),大家言必谈ReentrantLock,CAS,那么我也赶潮流CAS吧,主流程如下。
步骤 | 主要工作 | 失败流程 |
---|---|---|
1 | 无论是put还是take,先对head/tail getAndIncrease 在Array中占位 | 100%成功 |
2 | 检查是否能够放/取 | 失败则循环检查,条件允许了再操作(不能返回失败,第1步的占位无法回退) |
3 | 执行放/取操作 | – |
第2步的操作,如果失败,并非完全不能回退,只是需要牵扯到一套复杂的逻辑,在这个简单粗暴的实现中,先不考虑非阻塞方案。
核心代码如下:
private Object[] array;
private final int capacity;
private final int m;
private final AtomicLong tail;
private final AtomicLong head;
private final AtomicLong[] als = new AtomicLong[11];
public RiskBlockingQueue(int preferCapacity) {
this.capacity = getPow2Value(preferCapacity, MIN_CAPACITY, MAX_CAPACITY);
//这是个取比preferCapacity大的,最接近2的整数幂的函数(限制必须在 MIN MAX 之间)
array = new Object[this.capacity];
this.m = this.capacity - 1;
for (int i = 0; i < als.length; i++) { // 并不一定100%成功的伪共享padding
als[i] = new AtomicLong(0);
}
head = als[3];
tail = als[7];
}
public void put(T obj) {
ProgramError.when(obj == null, "Can't add null into this queue");
int p = (int) (head.getAndIncrement() & this.m);
int packTime = MIN_PACKTIME_NS;
while(array[p] != null) {
LockSupport.parkNanos(packTime);
if(packTime < MAX_PACKTIME_NS) packTime <<= 1;
}
array[p] = obj;
}
public T take(){
Object r;
int p = (int) (tail.getAndIncrement() & this.m);
int packTime = MIN_PACKTIME_NS;
while((r = array[p]) == null) {
LockSupport.parkNanos(packTime);
if(packTime < MAX_PACKTIME_NS) packTime <<= 1;
}
array[p] = null;
return (T)r;
}
代码简单的不要不要的,30来行代码,一个线程安全的阻塞就基本完成。什么?你问构造函数为什么叫RiskBlockingQueue?,很简单,有Risk,这并不是一个真正意义上的线程安全Queue,它有风险,那么风险在哪里呢?
各位看官先自己想想风险在哪里,我先来测个1P-1C 性能数据 (以下数据都在关闭了CPU超线程的环境下测试获得,超线程时数据经常看上去很美)
1P – 1C
Producer 0 has completed. Cost Per Put 19ns.
Consumer 0 has completed. Cost Per Take 19ns.
Total 201M I/O, cost 3844ms, 52374243/s(52M/s)
????,还真的和预测差不多,性能减半。
接下来,揭晓