【问题标题】:What causes a multithreading program hanging while switching threads?是什么导致多线程程序在切换线程时挂起?
【发布时间】:2013-01-01 08:31:18
【问题描述】:

我有一个多线程程序,它的线程数可以自定义。该程序负责生成 HTTP 请求,将请求发送到 Web 服务,接收响应并解析响应以获取一些结果值。

由于每个请求几乎需要 1 秒才能获得响应,为了让程序获得尽可能多的响应,启动了多个线程。

以下代码用于启动多线程:

    ...
    for (int i = 0; i < threadNum; i++) {
        threadArray[i] = new Thread(requestGeneratorArray[i]);
        threadArray[i].start();
    }

    for (int i = 0; i < threadNum; i++) {
        try {
            threadArray[i].join();
        } catch (InterruptedException e) {
            logger.error(e);
        }
    }

...

当线程数为50,一共产生并发送10K个请求时,程序运行良好。当线程数仍为50,总请求数为100K时。当发送 95K+ 请求时,程序挂起。也不例外,程序就挂在那里。

然后我添加一些像这样的 JVM 参数:java -Xmx1024M -Xms512M -XX:MaxPermSize=256m ... 使用这些参数,50 个线程/100K 请求有效。但是,50 个线程/1M 请求再次挂起。我将线程数设置为20,请求数设置为1M,它又工作了。

我想将线程数设置为 50,因为在使用较少请求数 (10K) 的情况下进行测试,50 个线程的效率最高。请求数可能比 10M、100M、事件 1B 大得多。在这种情况下,增加 -Xmx -Xms 或 MaxPermSize 的大小不是一个好主意。我该怎么办?程序挂起的根本原因是什么?

================================================ =============

我直接使用了 Executor Service 而不是线程。但是问题也出现了。我重新检查了代码,发现有一个错误:我为每个请求实例化了一个 HttpClient 对象。我将代码更改为为每个线程实例化一个 HttpClient 实例,并且程序不再挂起。

我认为程序挂起的根本原因是它与 Web 服务建立了太多的 HTTP 连接,并且耗尽了 Web 服务的所有线程。这使得 Web 服务无法响应任何新到达的请求。这是正确的吗?

【问题讨论】:

  • “待定”是什么意思?是挂的意思吗?有很多事情可能会影响到这一点,尤其是您要连接的网络服务器可能有最大数量的并发连接,这意味着您程序中的大多数线程都在等待网络服务器开始与它们对话。跨度>
  • 您可以通过使用内置线程池(例如Executors factory 提供的线程池之一)来让您的生活更轻松,而不是自己管理一个(但它可能无法解决您的问题)。
  • 如何发送请求?有多少线程在发送,有多少线程在解析请求?
  • 如果您希望您的应用程序扩展到任意 #/requests,也许您可​​能希望考虑使用 [线程池](docs.oracle.com/javase/tutorial/essential/concurrency/…),而不是为每个请求生成一个新线程。恕我直言...
  • @JohnFarrelly:是的,它应该是“挂起”而不是“待定”。我已与网络服务开发人员确认并确保没有连接限制。

标签: java multithreading


【解决方案1】:

仅从这些信息很难判断,但从堆设置会影响结果这一事实来看,我敢打赌,内容生成和内容解析(存储)之间的调度会很差。

此类应用程序中的一个常见情况是生成内容的线程比获取内容并将其存储的线程生成速度更快。这将逐渐增加用于在内存中保存内容的堆内存量,并且在某些时候吞吐量将开始直线下降。

首先要做的是通过附加一个像 VisualVM 这样的堆查看器来确认这个假设。如果您的堆使用量逐渐增加并开始固定在高水平并且您的吞吐量下降,这可能是罪魁祸首(您也可以确认您内存中的内容确实是生成的内容)。

通常,瓶颈是用于存储内容的持久层的 IO。您可能在解析代码(或其他地方)中遇到 CPU 瓶颈,具体取决于您的代码在做什么,但这通常很少见。

针对这种情况最常见的补救措施是使用有界队列让生成进程等待解析(存储)进程赶上来。看看这个 SO 答案:How to make ThreadPoolExecutor's submit() method block if it is saturated?。您将不得不了解线程池,但它确实是对原始线程的巨大改进,并且它是处理此类问题的最干净的方法。

【讨论】:

  • 如果堆满了,这不会触发 OutOfMemoryError 吗?
  • @Jan Dvorak:我不确定它是如何工作的,但我已经看到应用程序存活了很长时间,然后它们主要死于“GC 开销达到”OOME。当堆变得稀缺并且在某些情况下它可以工作很长时间时,也许 JVM GC 有一些策略可以使用?但我只是猜测。
  • 我并不知道“停止工作”的行为。但是,我从描述中了解到,应用程序突然停止,而不是逐渐停止。这似乎表明任务丢失,而不是 99.9% 的时间花在 GC 上。
  • @JanDvorak 你是对的。该程序实际上突然停止。但无一例外表明任何任务丢失。如何确认是否有任务丢失?
  • @JuliaLi:我要做的第一件事就是进行线程转储,看看发生了什么。
【解决方案2】:

+1 对使用Executor Service 的所有建议。

您还提到您正在使用 Http 客户端。很少有配置参数会make http client faster for concurrent usage

关于程序挂起。它可能是大型垃圾收集器运行的死锁。我相信像jconsoleVisual VM 这样的工具可以帮助您调试这两种情况。

【讨论】:

    【解决方案3】:

    由于我们不知道线程挂在哪里:您是否考虑过某种线程监视,使用分析器或使用 ThreadMXBeans 获取线程的定期堆栈跟踪? 正如其他一些海报所提到的,如果有任何可伸缩性问题,您也应该关注 gc.log。并观察你的记忆足迹。但这可能不是这里的问题,因为即使是完整的 gc 最终也应该完成,而您的程序却没有。

    【讨论】:

    • 是的,这是个好主意。但是,这么简单的程序是不是太复杂了?
    • 对于这个问题,实际上是:) 但您应该能够轻松地重用该代码。 VisualVM 应该让您无需编码即可获得线程堆栈跟踪。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-09
    • 1970-01-01
    相关资源
    最近更新 更多