前两天看了一篇文章Java里快如闪电的线程间通讯,对生产者消费者问题有了新的理解。
对于最普通的生产者消费者问题,我们通常采用一个队列的方式,队列的原理是缓存,使得生产者消费者不匹配的速度得以匹配。而队列的缺点也是显而易见的:每次读写都需要对整个队列上锁同步,读写次数增多时无疑是浪费。
题外话,对于队列的应用和好处其实远不止缓存。队列既能够实现线程间消息(数据结构),又能实现进程间消息(中间件产品如MQ),还可以在节点级别实现消息机制和解耦(消息服务器),这种解耦方式本身是一个非常牛X的思想。
用队列解决生产者消费者问题,似乎除了加锁,我们无能为力。然而,轻量级加锁,使用数组快速寻址以及增大吞吐量的ringbuffer的出现,似乎在昭示一种更加快速的解决方式,disruptor曾一度被推向风口浪尖,一次又一次的向人们证明自己的强大。
从ringbuffer的实现来说,无疑是增大并发的极好方式,因为它不会每次对整个队列加锁,而是采用cas来读写某个具体数据,这个方式是比较轻量的,另外,一次可以向前读取多个安全数据的做法也非常出色的减少了加锁的必要。
然而,disruptor的吞吐量在一定程度上还是有限。还有更好的办法吗?在文中介绍了railway的方式,实在令人耳目一新,但是仔细思考,其实这就是用多个缓存来增加吞吐量和减少锁定的时间,其思想就是用空间换时间。
其思想比较简单实用,考虑两辆列车,其中producer不断的向靠站的列车送人,而另一辆列车则不断靠站下人到consumer。考虑极端情况,生产者装满一车人,同时消费者下完一车人,这时候两辆列车都顺时针走到下一站。空车上人,满车下人,可以想象,如果列车较大,大多数时间都用来上下客,仅有少数的转换时间是无效率的。
淘宝的datax以及点评的wormhole都是采用这种方式,其特点就是吞吐量大,对于多个生产者和多个消费者的情形,可以通过增加列车的方式(即增加缓冲)来提升吞吐量。
比如wormhole,其核心实现是一个DoubleQueue,多个生产者插入的是push操作,而消费者则在pull操作队列为空时做一次queueSwitch切换缓冲区,doublequeue用了两个队列,两个锁(读锁和写锁)以及两个Condition(awake和notFull)。两个队列分别作为列车,两个锁分别锁定这两个队列,两个condition都是写锁上的,分别控制写队列满的情况和写队列空的情况,如果写队列满,则该线程需要等待,直到队列切换;如果写队列空,则队列切换需要等待,直到写队列写入第一个元素。