【问题标题】:FixedThreadPool not parallel enoughFixedThreadPool 不够并行
【发布时间】:2012-03-21 05:45:00
【问题描述】:

我使用forPool = Executors.newFixedThreadPool(poolSize); 创建了一个固定线程池,其中 poolSize 被初始化为处理器上的内核数(比如说 4)。在某些运行中,它运行良好,CPU 利用率始终保持在 400%。

但有时,使用率会下降到 100%,并且永远不会回升到 400%。我安排了 1000 多个任务,所以问题不在于。我捕获了每个异常,但没有抛出异常。所以这个问题是随机的,不可重现,但非常普遍。它们是数据并行操作。在每个线程结束时,都有一个同步访问来更新单个变量。我极不可能在那里陷入僵局。事实上,一旦我发现这个问题,如果我销毁池并创建一个大小为 4 的新池,它仍然只有 100% 的使用率。没有 I/O。

这似乎违背了 java 对“FixedThreadPool”的保证。我读错了保证书吗?只保证并发而不保证并行?

关于这个问题 - 你遇到过这个问题并解决了吗?如果我想要并行性,我做对了吗?

谢谢!

在进行线程转储时: 我发现有 4 个线程都在执行它们的并行操作。但是使用率仍然只有~100%。这是400% usage100% usage 的线程转储。我将线程数设置为 16 以触发场景。它以 400% 运行一段时间,然后降至 100%。当我使用 4 个线程时,它会以 400% 的速度运行,并且很少会降至 100%。 This 是并行化代码。

****** [重大更新] ** ****

事实证明,如果我给 JVM 大量的内存来玩,这个问题就解决了,性能也没有下降。但我不知道如何使用这些信息来解决这个问题。帮助!

【问题讨论】:

  • 您是否在“问题阶段”对程序进行了线程转储?
  • @Sanjeev 您的任务是否使用任何类型的同步?因为我们首先要假设你的任务可以完全并行运行。这是你的意思吗?
  • 执行的任务是什么——它有 IO 吗?您可以在线程转储为 100% 时捕获线程转储,并查看池中的四个线程在做什么。
  • 我正在尝试这样做,但我现在无法调用该场景。我会立即发布结果。
  • 这是一个 MemoryInfo 类,可用于在处理时记录内存统计信息:pastebin.com/Mpw3b3yy

标签: java multithreading parallel-processing executorservice


【解决方案1】:

鉴于增加堆大小会使问题“消失”(也许不是永久),这个问题可能与 GC 有关。

Operation 实现是否有可能在调用之间生成一些存储在堆上的状态

pOperation.perform(...);

?如果是这样,那么您可能有内存使用问题,可能是泄漏。随着更多任务的完成,更多的数据在堆上。垃圾收集器必须越来越努力地尝试尽可能多地回收,逐渐占用您总可用 CPU 资源的 75%。即使销毁 ThreadPool 也无济于事,因为这不是存储引用的地方,而是在操作中。

16 线程案例更多地遇到这个问题可能是因为它更快地生成更多状态(不知道操作实现,我很难说)。

在保持问题设置不变的情况下增加堆大小会使这个问题看起来消失,因为你有更多的空间来处理所有这些状态。

【讨论】:

  • 我也是这么想的。如果内存使用量不断增长,那么肯定存在内存泄漏,但我自己似乎无法确定 - 所有引用都会重新初始化,并且显式调用 gc 也不会减少内存使用量:(
  • 在我看来有两种可能性:一种是您遇到了与保存 Operation.perform() 的结果相关的内存使用问题,另一种更像是泄漏。为了弄清楚是哪一个,我会更改 perform() 以便结果不会存储在内存中:只需将它们丢弃以进行测试即可。如果这不能解决问题,那么假设泄漏:继续删除结果,在分析器下运行此测试(我喜欢 JProfiler,但无论如何),然后查看您的内存去向。这应该缩小范围,除非问题出在本机代码中。
  • @SanjeevSatheesh 抱歉,忘记在我的评论中提及您。
  • 是后一种情况,存在内存泄漏。我尝试了分析,但由于数据量大,eclipse死了:/所以分析路线没有成功。从我从分析器中得到的任何内容来看,最大的内存量来自我正在创建的小矩阵,但它们都是短命的~1s。所以我回到第一方。
  • @SanjeevSatheesh 找到它并不是一件容易的事,但有一些建议:1) 如果您认为 Eclipse 给您带来了麻烦,请不要考虑它。大多数分析工具都可以在编译后的代码上工作,您不必使用 IDE 调试器。 2)因为您可以可靠地重现该问题,所以可以进行蛮力追踪。只需继续从 .perform() 中删除功能即可。 3)如果你有足够的短命、相对大的对象,你可能真的有一个调整问题。从这里开始:oracle.com/technetwork/java/javase/gc-tuning-6-140523.html
【解决方案2】:

我建议您使用Yourkit Thread Analysis 功能来了解实际行为。它会准确告诉您哪些线程正在运行、阻塞或等待以及原因。

如果您不能/不想购买它,下一个最佳选择是使用与 JDK 捆绑在一起的Visual VM 来执行此分析。它不会像 Yourkit 那样为您提供详细的信息。以下博客文章可以帮助您开始使用 Visual VM: http://marxsoftware.blogspot.in/2009/06/thread-analysis-with-visualvm.html

【讨论】:

  • 让我看看。谢谢。
  • 顺便说一句,您可以获得 YourKit 的短期评估许可证,因此您无需购买即可试用。 ;)
【解决方案3】:

我的回答是基于有关 JVM 内存管理的知识和一些关于我无法找到准确信息的事实的猜测。我相信您的问题与 Java 使用的线程本地分配缓冲区 (TLAB) 有关:

线程局部分配缓冲区 (TLAB) 是 Eden 的一个区域,它是 用于由单个线程分配。它使线程能够做 使用线程局部顶部和限制指针进行对象分配,即 比对共享的顶部指针执行原子操作更快 跨线程。

假设您有一个 2M 的 eden 大小并使用 4 个线程:JVM 可能会选择 (eden/64)=32K 的 TLAB 大小,并且每个线程都会获得该大小的 TLAB。一旦线程的 32K TLAB 用完,就需要重新获取一个,这需要全局同步。分配大于 TLAB 的对象也需要全局同步。

但是,老实说,事情并不像我描述的那么简单:JVM 根据在次要 GC [1] 确定的估计分配率自适应地调整线程的 TLAB,这使得与 TLAB 相关的行为更少可预见。但是,我可以想象当更多线程在工作时,JVM 会缩小 TLAB 的大小。这似乎是有道理的,因为所有 TLAB 的总和必须小于可用的 eden 空间(实际上甚至是 eden 空间的一部分才能重新填充 TLAB)。

让我们假设每个线程的固定 TLAB 大小为(eden 大小/(16 * 用户线程工作)):

  • 对于 4 个线程,这会导致 32K 的 TLAB
  • 对于 16 个线程,这会产生 8K 的 TLAB

您可以想象,由于 TLAB 更小,16 个线程会更快耗尽其 TLAB,这将导致 TLAB 分配器上的锁定比 32K TLAB 的 4 个线程多得多。

总而言之,当您减少工作线程数或增加 JVM 可用内存时,可以为线程分配更大的 TLAB,问题就解决了。

https://blogs.oracle.com/daviddetlefs/entry/tlab_sizing_an_annoying_little

【讨论】:

    【解决方案4】:

    这几乎可以肯定是由于 GC。

    如果您想确保将以下启动标志添加到您的 Java 程序中:
    -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 并检查标准输出。

    您将看到包含“Full GC”的行,包括此所用的时间:在此期间,您将看到 100% 的 CPU 使用率。

    多 CPU 或多核机器上的默认垃圾收集器是吞吐量收集器,它并行收集年轻代,但对老年代使用串行收集(在一个线程中)。

    所以可能发生的情况是,在您的 100% CPU 示例中,GC 正在老一代中进行,它在一个线程中完成,因此只保持一个核心忙碌。

    解决方案建议:使用并发标记和清除收集器,在 JVM 启动时使用标志
    -XX:+UseConcMarkSweepGC

    【讨论】:

      【解决方案5】:

      调整 JVM

      Java 平台的核心是 Java 虚拟机 (JVM)。整个 Java 应用程序服务器在 JVM 中运行。 JVM 将许多启动参数作为命令行标志,其中一些参数对应用程序性能有很大影响。那么,让我们来看看服务器应用程序的一些重要的 JVM 参数。

      首先,您应该使用 -Xms(最小内存)和 -Xmx(最大内存)标志为 JVM 分配尽可能多的内存。例如,-Xms1g -Xmx1g 标签为 JVM 分配 1GB RAM。如果您未在 JVM 启动标志中指定内存大小,则 JVM 会将堆内存限制为 64MB(Linux 上为 512MB),无论您在服务器上有多少物理内存!更多内存允许应用程序处理更多并发 Web 会话,并缓存更多数据以改善缓慢的 I/O 和数据库操作。我们通常为两个标志指定相同数量的内存,以强制服务器从启动时使用所有分配的内存。这样,JVM 就不需要在运行时动态更改堆大小,这是导致 JVM 不稳定的主要原因。对于 64 位服务器,请确保在 64 位操作系统之上运行 64 位 JVM,以利用服务器上的所有 RAM。否则,JVM 将只能使用 2GB 或更少的内存空间。 64 位 JVM 通常仅适用于 JDK 5.0。

      如果堆内存很大,垃圾收集 (GC) 操作可能会成为主要的性能瓶颈。 GC 扫描数 GB 堆可能需要十多秒。在 JDK 1.3 及更早版本中,GC 是单线程操作,它会停止 JVM 中的所有其他任务。这不仅会导致应用程序出现长时间且不可预测的暂停,而且还会导致多 CPU 计算机的性能非常差,因为所有其他 CPU 必须在一个 CPU 以 100% 运行以释放堆内存空间时处于空闲状态。选择支持并行和并发 GC 操作的 JDK 1.4+ JVM 至关重要。实际上,JDK 1.4 系列 JVM 中的并发 GC 实现并不是很稳定。因此,我们强烈建议您升级到 JDK 5.0。使用命令行标志,您可以从以下两种 GC 算法中进行选择。它们都针对多 CPU 计算机进行了优化。

      • 如果您的首要任务是增加 应用程序并且您可以容忍偶尔的 GC 暂停,您应该使用 -XX:UseParallelGC 和 -XX:UseParallelOldGC(后者仅 在 JDK 5.0 中可用)标志来打开并行 GC。并行GC 使用所有可用的 CPU 来执行 GC 操作,因此它是 比默认的单线程 GC 快得多。它仍然暂停所有 然而,在 GC 期间 JVM 中的其他活动。
      • 如果需要最小化 GC 暂停,可以使用 -XX:+UseConcMarkSweepGC 标志开启并发GC。并发 GC 仍然暂停 JVM 并使用并行 GC 进行清理 寿命短的物体。但是,它会从 使用与其他 JVM 并行运行的后台线程的堆 线程。并发 GC 大大减少了 GC 暂停,但是 管理后台线程确实增加了系统的开销 并降低总吞吐量。

      此外,您还可以调整一些 JVM 参数来优化 GC 操作。

      • 在 64 位系统上,每个线程的调用堆栈分配 1MB 内存空间。大多数线程不使用那么多空间。使用 -XX:ThreadStackSize=256k 标志,您可以将堆栈大小减小到 256k 以允许更多线程。
      • 使用 -XX:+DisableExplicitGC 标志忽略显式应用程序 调用 System.gc()。如果应用程序调用此方法 经常,那么我们可能会做很多不必要的 GC。
      • -Xmn 标志让您可以手动设置“young 代”内存空间用于短期对象。如果您的应用程序 生成大量新对象,您可以通过以下方式显着改进 GC 增加这个值。 “年轻一代”的规模应该差不多 永远不要超过堆的 50%。

      由于 GC 对性能有很大影响,JVM 提供了几个标志来帮助您针对特定服务器和应用程序微调 GC 算法。详细讨论 GC 算法和调优技巧超出了本文的范围,但我们想指出,JDK 5.0 JVM 带有一个称为人体工程学的自适应 GC 调优特性。它可以根据底层硬件、应用程序本身以及用户指定的期望目标(例如,最大暂停时间和期望吞吐量)自动优化 GC 算法参数。这样可以节省您自己尝试不同 GC 参数组合的时间。人机工程学是升级到 JDK 5.0 的另一个令人信服的理由。有兴趣的读者可以参考使用 5.0 Java 虚拟机调优垃圾收集。如果 GC 算法配置错误,那么在应用程序的测试阶段就相对容易发现问题。在后面的部分中,我们将讨论几种诊断 JVM 中 GC 问题的方法。

      最后,确保使用 -server 标志启动 JVM。它优化了即时 (JIT) 编译器,以较慢的启动时间换取更快的运行时性能。还有更多我们没有讨论过的 JVM 标志;有关这些的详细信息,请查看 JVM 选项文档页面。

      参考: http://onjava.com/onjava/2006/11/01/scaling-enterprise-java-on-64-bit-multi-core.html

      【讨论】:

        【解决方案6】:

        100% 的总 cpu 利用率暗示您编写的是单线程的。即您可能有任意数量的并发任务,但由于锁定,一次只能执行一个。

        如果您的 IO 较高,您可以获得低于 400% 的 CPU 利用率,但您不太可能获得整数的 CPU 利用率。例如您可能会看到 38%、259%、72%、9% 等(也可能会跳来跳去)

        一个常见问题是锁定您经常使用的数据。您需要考虑如何在整个工作的最短时间和最小部分执行锁定的情况下重写它。理想情况下,您希望避免全部锁定。

        使用多线程意味着您最多可以使用那么多 CPU,但如果您的代码阻止了它,您可能会更好(即更快)编写单线程代码,因为它可以避免锁定开销。

        【讨论】:

        • 没有 I/O,锁定几乎是微不足道的。就像我说的,在这个阶段进行线程转储时,所有线程都存在并执行它们独立的操作 - 这种减速不是由于阻塞。
        • 在你知道它是什么之前,你不能说它不是由于阻塞。你能展示使用四个线程的线程转储做他们的事情吗?
        • 每个线程都在处理繁重的工作,唯一同步的部分是更新单个变量,我围绕该变量有一个获取和释放锁。在进行线程转储时,我发现它们都处于处理例程的中间,并且没有阻塞这个锁。如果您愿意,我可以将转储发布到某个地方。
        • 在问题中作为代码块会很好。当它使用 100% 而不是 400% 时只有四个线程。
        • 完成!我必须使用 16 个线程来触发场景,所以作为引用发布太多了,所以我将它们放在 pastebin 上。
        【解决方案7】:

        由于您正在使用锁定,因此您的四个线程中的一个可能会获得锁定,但随后会切换上下文 - 可能是为了运行 GC 线程。其他线程无法取得进展,因为它们无法获得锁。当线程上下文切换回来时,它完成了关键部分的工作并放弃了锁,只允许另一个线程获得锁。所以现在你有两个线程处于活动状态。有可能当第二个线程执行临界区时,第一个线程执行下一条数据并行工作,但产生了足够的垃圾来触发 GC,我们又回到了开始的地方:)

        附:这只是一个最好的猜测,因为没有任何代码 sn-ps 很难弄清楚发生了什么。

        【讨论】:

          【解决方案8】:

          增加 Java 堆的大小通常会提高吞吐量,直到堆不再驻留在物理内存中。当堆大小超过物理内存时,堆开始交换到磁盘,这导致 Java 性能急剧下降。因此,将最大堆大小设置为允许堆包含在物理内存中的值非常重要。

          由于您在机器上为 JVM 分配了约 90% 的物理内存,因此当您尝试为更多对象分配内存时,问题可能与由于内存分页和交换而发生的 IO 有关。请注意,其他正在运行的进程以及操作系统也使用物理内存。此外,由于症状会在一段时间后出现,这也是内存泄漏的迹象。

          尝试找出有多少物理内存可用(尚未 已使用)并将大约 90% 的可用物理内存分配给您的 JVM 堆。

          • 如果您让系统长时间运行会发生什么 时间?

          • CPU 利用率是否会恢复到 400%?

          • 当 CPU 利用率为 100% 时,您是否注意到任何磁盘活动?
          • 能否监控哪些线程正在运行,哪些线程被阻塞? 什么时候?

          查看以下链接进行调整: http://java.sun.com/performance/reference/whitepapers/tuning.html#section4

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2015-12-23
            • 1970-01-01
            • 2012-07-22
            • 2020-09-10
            • 1970-01-01
            • 1970-01-01
            • 2014-01-30
            相关资源
            最近更新 更多