Java中使用LinkedBlockingQueue存在性能问题

16

这是我在stackoverflow上的第一篇帖子...希望��人可以帮助我。

我使用Java 6的LinkedBlockingQueue时出现了性能大幅下降的问题。在第一个线程中,我生成一些对象并将它们推入队列中,在第二个线程中,我将这些对象取出。当频繁调用LinkedBlockingQueuetake()方法时,性能回归就会发生。我监控了整个程序,take()方法总体上占用了最多的时间,并且吞吐量从大约58Mb/s降至0.9Mb/s......

队列的弹出和获取方法是通过该类的静态方法调用的。

public class C_myMessageQueue {

    private static final LinkedBlockingQueue<C_myMessageObject> x_queue = new LinkedBlockingQueue<C_myMessageObject>( 50000 );

    /**
     * @param message
     * @throws InterruptedException
     * @throws NullPointerException
     */
    public static void addMyMessage( C_myMessageObject message )
            throws InterruptedException, NullPointerException {
        x_queue.put( message );
    }

    /**
     * @return Die erste message der MesseageQueue
     * @throws InterruptedException
     */
    public static C_myMessageObject getMyMessage() throws InterruptedException {
        return x_queue.take();
    }
}

如何调整take()方法,以实现至少25Mb/s的速度,或者是否有其他类可以使用,当“队列”满或空时将阻塞。

顺祝商祺

Bart

附言:抱歉我的英语不好,我来自德国;)


1
MB/s 是没有意义的。take() 得到一个引用。除了引用本身的字,负载是几个字节还是许多兆字节对队列来说都是无关紧要的。 - Dimitris Andreou
我知道...但是我试图强制回归的广泛性 ;) - lofthouses
你能先尝试向队列中添加5000个元素,然后运行take操作,看看每个take操作花费了多长时间吗? - John Vint
如果您正在使用JDK 6,您是否遇到了这个错误?http://bugs.sun.com/view_bug.do?bug_id=6805775 - kommradHomer
8个回答

17
您的生产者线程仅仅是放置了比消费者消费更多的元素,因此队列最终达到其容量限制,从而使生产者等待。
总结原始答案后,现在我们基本上已经知道完整情况:
  • 通过非常快速的put(),您触发了LinkedBlockingQueue固有的吞吐量限制(每个队列都有一个),即使是连续的take()也无法跟上,而且没有进一步的处理。(顺便说一句,这表明在此结构中,在您的JVM和计算机上,put()至少比读取更耗费成本)。
  • 由于存在特定的锁,消费者锁定它,因此增加更多的消费者线程不可能有所帮助(如果您的消费者实际上正在做一些处理并且这是限制吞吐量的原因,那么添加更多的消费者将有所帮助。对于具有多个消费者或生产者的场景,有更好的队列实现,您可以尝试使用SynchronousQueueConcurrentLinkedQueue以及即将推出的jsr166y的TransferQueue)。
一些建议:
  • 尝试制作更粗粒度的对象,以便队列每个操作的开销与实际从生产线程卸载的工作平衡(在您的情况下,似乎您为代表可以忽略的工作量的对象创建了大量通信开销)。
  • 生产者也可以通过卸载一些消费工作来帮助消费者(如果有工作可做,无意义地等待没有多少意义)。
/ 在John W.正确指出我的原始答案误导人之后更新

我不相信在消费线程中使用StringBuilder.append(" ------")比生产线程中的许多算术操作慢得多。 - lofthouses
我怀疑你可能不必要地创建了非常细粒度的对象。如果需要的话,您还可以尝试将其中一些捆绑在更大的包中以减少队列开销。 - Dimitris Andreou
我尝试了几种队列容量,但是在20或50000个元素时都出现了相同的问题...目前我正在尝试使用队列传输对象列表的方法。 - lofthouses
如果消费者花费了很长时间,那么队列就是空的,对吗?他会想增加生产者的数量。 - John Vint
没错,我只是不明白队列已满且生产者在等待与take花费大量时间处理有什么关系。也许这就是你的观点,但是你的答案会误导OP认为take的速度下降受到生产者的影响。 - John Vint
显示剩余6条评论

6

我通常建议不要在性能敏感的代码区域中使用LinkedBlockingQueue,而是使用ArrayBlockingQueue。它将提供更好的垃圾收集配置文件,并且比LinkedBlockingQueue更适合缓存。

尝试使用ArrayBlockingQueue并测量性能。

LinkedBlockingQueue的唯一优点是它可以是无界的,但这很少是您实际想要的。如果消费者失败并且队列开始积压,则有界队列允许系统优雅地降级,而不是冒着队列无限制可能导致OutOfMemoryErrors的风险。


你是对的,但是arrayBQ和linkedBQ之间只有很小的性能差异。而且回归仍然存在。 - lofthouses

5
以下是需要翻译的内容:

这里有几个尝试的方法:

LinkedBlockingQueue 替换为 ArrayBlockingQueue。它没有悬空引用,因此在队列填满时表现更好。特别地,在 LinkedBlockingQueue 的 1.6 实现中,元素的 full GC 将不会发生,直到队列实际变为空。

如果生产者方始终优于消费者方,请考虑使用 draindrainTo 执行“批量”取操作。

或者,让队列接受消息对象的数组或列表。生产者用消息对象填充列表或数组,每个 put 或 take 移动多个消息,具有相同的锁定开销。将其视为秘书递给您一堆“你不在的时候”消息与一个接一个地递给您的方式。


该死,为什么我没自己试一下呢...那我现在就试试吧 :) - lofthouses
我怀疑 drainTo() 方法并不能解决问题。消费者的问题不在于争用消费者锁(因为只有一个消费者线程,获取消费者锁是无争议且非常快的)。根本问题在于消费者速度慢,数据流量较少,因此生产者无论如何(除非队列溢出),都会受到这种较少流量的限制 - 请参见我的答案。 - Dimitris Andreou
ArrayBlockingQueue 会比 LinkedBlockingQueue 慢,因为它在 put() 和 take() 方法中使用单个锁,而 LinkedBlockingQueue 在 put() 和 take() 方法中使用 2 个锁,并且只在队列为空/满时同步生产者和消费者。 - Amrish Pandey

1

不了解填充过程很难说会发生什么。

如果 addMyMessage 调用频率较低 - 可能是因为应用程序的其他部分存在性能问题 - 那么 take 方法就必须等待。

这样看起来像是 take 有问题,但实际上是应用程序的填充部分有问题。


填充线程不是罪魁祸首,当吞吐量下降时(使用visualvm分析器可见),填充线程在等待消费者线程。当填充线程无需等待消费者时,吞吐量非常好,至少为58Mb/s。消费者线程在take方法上等待,消费任务中的操作很少...我也尝试了ArrayBlockingQueue...同样的情况,消费者等待缓慢的take方法,填充等待因为队列已满。附:该程序是复制任务。 附言:谢谢您的回答 ;) - lofthouses

1

0
如果从您的阻塞队列中放置和获取对象的原始性能开销是瓶颈(而不是慢生产者/消费者问题),则可以通过批处理对象获得巨大的性能提升:例如,您可以放置或获取粗粒度对象列表,而不是细粒度对象。以下是代码片段:
ArrayBlockingQueue<List<Object>> Q = new ArrayBlockingQueue<List<Object>>();

// producer side
List<Object> l = new ArrayList<Object>();
for (int i=0; i<100; i++) {
    l.add(i); // your initialization here
}
Q.put(l);

// consumer side
List<Object> l2 = Q.take();
// do something 

批处理可以将您的性能提升一个数量级。

0

你的应用程序可能受到Java 6中与锁相关的更改的影响,特别是“偏向锁”功能。

尝试使用-XX:-UseBiasedLocking开关禁用它,看看是否有所改善。

请参阅此处以获取更多信息:Java SE 6性能白皮书:第2.1节


谢谢你提供的线索,但是没有改变什么...take()方法在消费者线程中占据了96.7%的时间。我认为take方法太慢了... - lofthouses
也许如果您查看该方法的实现,会有所帮助。我在其中没有发现任何奇怪的东西。 - Daniel Rikowski
我找不到任何问题所在,但事实是在这个线程中take操作占用了大部分时间。 - lofthouses

0

无法确定任何事情。但您可以尝试更改BlockingQueue实现(只是作为实验)。

您设置了初始容量50k并使用LinkedBlockingQueue。尝试使用相同容量的ArrayBlockingQueue,您还可以尝试调整fair参数。


这将是我的最后一次尝试...然后我会尝试使用Javolution或类似的东西...或者这是个坏主意吗? - lofthouses
我认为他指的是单生产者、单消费者场景,因此公平性应该无关紧要。有趣的是,LinkedBlockingQueue却没有暴露公平参数。 - Dimitris Andreou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接