2013-10-05 33 views
3

此问题涉及Java的最新版本。Java和队列:多线程I/O的饱和问题

30生产者线程将字符串推送到抽象队列。一个写线程从同一队列中弹出并将该字符串写入驻留在5400 rpm HDD RAID阵列上的文件。数据以大约111MBps的速率推送,并以大约80MBps的速率弹出/写入。该程序持续5600秒,足以让队列中积累大约176 GB的数据。另一方面,我限制在总共64GB的主内存中。

我的问题是:我应该使用什么类型的队列?

这是我到目前为止尝试过的。

1)ArrayBlockingQueue。这个有界的队列的问题是,无论数组的初始大小如何,我总是会在满足时立即生存活性问题。实际上,程序启动后几秒钟,top只报告一个活动线程。分析表明,平均而言,生产者线程花费大部分时间等待队列释放。这与我是否使用公平访问策略(将构造函数中的第二个参数设置为true)无关。

2)ConcurrentLinkedQueue。就活力而言,这个无界的队列表现更好。在内存耗尽之前,大约七百秒钟内,所有三十个生产者线程都处于活动状态。然而,在我超过了64GB的限制后,事情变得非常慢。我猜想这是因为分页问题,​​尽管我没有进行任何实验来证明这一点。

我预见了两种情况。

1)购买SSD。希望I/O速度的提高会有所帮助。

2)在写入文件之前压缩输出流。

是否有替代方案?我是否在构建/使用上述队列中缺少某些东西?有没有更聪明的方法来使用它们? Java Concurrency in Practice书中提出了许多饱和策略(见第8.3.3节),当有界队列填满的速度比他们可以耗尽的情况快时,但不幸的是他们中没有一个 - 中止,主叫运行和两个丢弃政策---适用于我的情况。

+0

当您意识到磁盘是您的瓶颈,无论您如何使用该软件,磁盘的性能都是您必须解决的问题。这意味着压缩数据和/或购买更快的磁盘。请注意,您可以获得大约100美元的400 MB/s SSD。 –

+1

如果您的生产者比您的消费者生产速度快,您必须让消费者更快或者生产者更慢。 – Raedwald

+0

您似乎很惊讶,试图在64GB计算机上存储176GB的对象会让您的程序变慢。 – Raedwald

回答

3

寻找瓶颈。由于你不想耗尽内存,因此你生产的产品多于消费,因此有界的队列是绝对有意义的。

试着让消费者更快。配置文件并查看最耗时的地方。由于您在此处写入磁盘有一些想法:

  • 您能否使用NIO解决您的问题? (也许FileChannel#transferTo()
  • 只在需要时冲洗。
  • 如果你有足够的CPU储备,压缩流? (因为你已经提到)
  • 优化您的磁盘速度(RAID缓存等)
  • 更快的磁盘

正如@Flavio已经说过,对于生产者 - 消费者模式,我看有没有问题它应该是现在的样子。最后一方控制速度。

+0

实际上使用有界队列会减慢生产者,直到他们的总速度与消费者的速度相匹配。 – Raedwald

+0

我接受这个答案,因为提到了NIO,我不知道。谢谢。 – user2650947

1

据java的 “Queue implementation” 有其他类,应该为你是正确的:

  • 的LinkedBlockingQueue
  • 的PriorityBlockingQueue
  • DelayQueue
  • 的SynchronousQueue
  • LinkedTransferQueue
  • TransferQueue

我不知道这些类的性能或内存使用情况,但您可以自己尝试。

我希望这可以帮助你。

2

我在这里看不到问题。在生产者 - 消费者的情况下,系统将始终以慢速派对的速度前进。如果生产者比消费者更快,当队列填满时,它将放慢到消费者的速度。

如果你的约束是你不能拖慢生产者,你将不得不寻找一种方法来加速消费者。描述消费者(不要太花哨,几个System.nanoTime()调用通常会提供足够的信息),检查它花费大部分时间的位置,并从那里开始优化。如果你有一个CPU瓶颈,你可以改进你的算法,添加更多的线程等。如果你有磁盘瓶颈尝试写少(压缩是一个好主意),得到一个更快的磁盘,写在两个磁盘,而不是一个...

1

为什么你有30个生产者。该数字是由问题域固定的,还是仅仅是您选择的数字?如果是后者,你应该减少生产者的数量,直到它们以大于消费量的总速率生产,并使用阻塞队列(如其他人所建议的)。然后,您将保持消费者繁忙,这是性能限制部分,同时尽量减少对其他资源(内存,线程)的使用。

1

你只有两个出路:让供应商变得更慢或消费者更快。生产速度较慢的生产者可以用很多方式完成,特别是使用有界的队列。为了让消费者更快,请尝试https://www.google.ru/search?q=java+memory-mapped+file。看看https://github.com/peter-lawrey/Java-Chronicle

另一种方法是从字符串准备写入缓冲区的工作中释放写入线程。让生产者线程发出就绪缓冲区,而不是字符串。使用有限数量的缓冲区,例如2 * threadnumber = 60。在开始时分配所有缓冲区,然后重新使用它们。为空缓冲区使用队列。生成线程从该队列获取一个缓冲区,填充它并放入写入队列。写入线程从写入线程获取缓冲区,写入磁盘并放入空缓冲区队列。另一种方法是使用asynchronous I/O。制作人自己开始写作,没有特别的写作线程。 Completion handler将使用的缓冲区返回到空缓冲区队列中。