FixedThreadPool不够平行

时间:2012-03-21 05:45:00

标签: java multithreading parallel-processing executorservice

我使用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提供了大量的内存,这个问题就解决了,性能也没有下降。但我不知道如何使用这些信息来解决这个问题。救命!

8 个答案:

答案 0 :(得分:5)

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

操作实现是否可能在调用

之间生成一些存储在堆上的状态
pOperation.perform(...);

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

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

在保持问题设置相同的同时增加堆大小会使这个问题看起来消失,因为你有足够的空间来处理所有这些状态。

答案 1 :(得分:2)

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

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

答案 2 :(得分:2)

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

  

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

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

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

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

  • 对于4个线程,这导致TLAB为32K
  • 对于16个线程,这导致TLAB为8K

你可以想象16个线程因为它们更小而耗尽TLAB的速度将导致TLAB分配器上的锁定比使用32K TLAB的4个线程更多。

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

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

答案 3 :(得分:1)

这几乎可以归结为GC。

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

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

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

所以可能发生的事情是,在您的100%CPU示例中,GC正在进行旧一代,这是在一个线程中完成的,因此只保留一个核心。

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

答案 4 :(得分:1)

调整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可能需要十几秒才能扫描多个千兆字节的堆。在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标志允许您手动设置"年轻人的大小 代"短期对象的内存空间。如果你的申请 生成大量新对象,你可能会大大改善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

答案 5 :(得分:0)

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

如果你有高IO,你可以得到低于400%,但你不太可能得到一个圆形的CPU利用率。例如你可能会看到38%,259%,72%,9%等(它也可能会跳转)

常见问题是过于频繁地锁定您正在使用的数据。您需要考虑如何在最短时间内执行锁定以及整体工作的最小部分重写。理想情况下,您希望避免将所有内容锁定在一起。

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

答案 6 :(得分:0)

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

P.S。这只是一个最好的猜测,因为如果没有任何代码片段很难弄清楚会发生什么。

答案 7 :(得分:0)

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

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

  

尝试找出可用的物理内存量(尚未提供)   使用)并将~90%的可用物理内存分配给JVM堆。

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

  • CPU 400%的使用率是否会回来?

  • 当CPU处于100%利用率时,您是否注意到任何磁盘活动?
  • 您可以监控哪些线程正在运行以及哪些线程被阻止 当?

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