【问题标题】:Java threading optimization at 100% CPU usage100% CPU 使用率的 Java 线程优化
【发布时间】:2012-04-12 02:09:16
【问题描述】:

我有一个应用程序接受队列上的工作,然后旋转该工作以在独立线程上完成。线程的数量并不多,比如最多 100 个,但这些都是密集型任务,可以快速将 CPU 提升到 100%。

为了以最快的速度完成最多的工作:我最好是在需要做更多工作时启动更多线程并让 Java 线程调度程序处理工作分配,还是变得更智能并管理工作负载以保持低于 100% 的 CPU 让我更快?

这台机器专用于我的 java 应用程序。

编辑:

感谢您的精彩意见!

任务具有不同的复杂性并涉及 I/O,因此如果线程池较低(例如 4)很可能仅将 CPU 运行到 20%。我无法知道有多少任务实际上会占用 CPU 到 100%。

我的想法是我应该通过 RMI 监控 CPU 并动态地上下拨动工作,还是应该不关心,让操作系统来处理它。

【问题讨论】:

  • 使用具有与处理器一样多的线程的固定大小的线程池。 100% 不一定是坏事,但您无法通过仅查看 CPU 使用率来判断 100% CPU 时间与过载情况(如颠簸)的最佳使用情况。
  • 我以为你在使用一个有 100 个线程的池。是否为每个任务创建一个新线程?如果是这样,请立即停止这样做,并按照@trutheality 的建议使用一个池
  • @Steve 关于您的编辑,如果您可以将线程分为 CPU 密集型线程和 IO 线程,那么您可以将 CPU 密集型线程放入 #-of-cores 池中,并让 IO线程在没有池的情况下产生。这在理论上应该给你最好的用法。

标签: java multithreading performance


【解决方案1】:

如果您在并行线程中有太多同时计算密集型任务,您会很快达到收益递减点。事实上,如果有 N 个处理器(内核),那么您不想要超过 N 个这样的线程。现在,如果任务偶尔会因 I/O 或用户交互而暂停,那么正确的数字可能会更大一些。但一般来说,如果在任何时候想要进行计算的线程多于可用的内核数,那么您的程序就是在上下文切换上浪费时间——也就是说,调度会花费您。

【讨论】:

  • 它的成本是多少?如果有很多 CPU 密集型任务,机器过载并且就绪线程总是比处理器多,那么操作系统很可能会在每个 imer 中断时更改就绪线程的集合。因此,无论有多少 CPU 绑定线程,上下文切换的数量都将被限制为每 .. 30 毫秒或其他任何时间。这并不重要。你上面说的是真的,但没有任何理由让用户进行额外的线程微管理(这经常出错)。
  • @MartinJames -- 我几乎不认为选择最佳大小的线程池是微观管理。无论如何:请引用。您的陈述与所有标准文本和参考文献背道而驰。只是一个同意我的例子:ibm.com/developerworks/library/j-jtp0730/index.html.
  • 更多实证结果。 C++ CPU 密集型任务。 i7、3GHz、4 核(8 w. hyper)、12GB RAM。 ticks/poolThreadCount/taskManagerCPU:356/8/34、287/16/29、280/80/30、284/800/28。最佳池线程数大于 [核心数],而且明显如此。到目前为止,如果您希望 CPU 密集型任务尽可能快地运行,请使用 80 个线程。如果您希望它们尽可能高效地运行,请使用 800。即使我觉得这不合理,所以有人证明我错了......
  • @Gray - 事实证明。我的任务很无聊,只是照常做——增加一个整数成员。我在 for 循环中添加了另一个“0”。现在计数到 100000000,我排了 400 个。我在测试期间的 CPU 使用率现在在所有 4/HT8 内核上都是 100%。到目前为止的 Ticks/threadCount:21922/8、20424/80、20191/800。 800线程更好! CPU 风扇发出很大的噪音,而且这里变热了。
  • 不过,这是一个有趣的问题。多年来,我看到有关仅创建 [no.由于“上下文切换的开销”,核心] 线程以获得最佳性能。现在我似乎发现最好的线程数比这多得多,即使是 CPU 密集型作业。
【解决方案2】:

您的 CPU 以 100% 运行这一事实并不能说明他们在做有用工作的忙碌程度。在您的情况下,您使用的线程多于内核,因此 100% 包含一些上下文切换并不必要地使用内存(对 100 个线程的影响很小),这是次优的。

对于CPU密集型任务,我一般用这个成语:

private final int NUM_THREADS = Runtime.getRuntime().availableProcessors() + 1;
private final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);

正如其他人所指出的,使用更多线程只会引入不必要的上下文切换。

显然,如果任务执行一些 I/O 和其他阻塞操作,这是不适用的,更大的池是有意义的。

编辑

为了回复@MartinJames 的评论,我运行了一个(简单的)基准测试——结果表明,从池大小 = 处理器数量 + 1 到 100 只会稍微降低性能(我们称之为 5%)——去更高的数字(1000 和 10000)确实会显着影响性能。

结果是 10 次运行的平均值:
池大小:9:238 毫秒。 //(NUM_CORES+1)
池大小:100:245 毫秒。
池大小:1000:319 毫秒。
池大小:10000:2482 毫秒。

代码:

public class Test {

    private final static int NUM_CORES = Runtime.getRuntime().availableProcessors();
    private static long count;
    private static Runnable r = new Runnable() {

        @Override
        public void run() {
            int count = 0;
            for (int i = 0; i < 100_000; i++) {
                count += i;
            }
            Test.count += count;
        }
    };

    public static void main(String[] args) throws Exception {
        //warmup
        runWith(10);

        //test
        runWith(NUM_CORES + 1);
        runWith(100);
        runWith(1000);
        runWith(10000);
    }

    private static void runWith(int poolSize) throws InterruptedException {
        long average = 0;
        for (int run = 0; run < 10; run++) { //run 10 times and take the average
            Test.count = 0;
            ExecutorService executor = Executors.newFixedThreadPool(poolSize);
            long start = System.nanoTime();
            for (int i = 0; i < 50000; i++) {
                executor.submit(r);
            }
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);
            long end = System.nanoTime();
            average += ((end - start) / 1000000);
            System.gc();
        }
        System.out.println("Pool size: " + poolSize + ": " + average / 10 + " ms.  ");
    }
}

【讨论】:

  • '使用更多线程,正如其他人指出的那样,只会引入不必要的上下文切换'当就绪线程数等于内核数时,这就是你将要获得的上下文切换。添加更多线程不会显着增加开销。
  • 谢谢,我应该添加'更多线程不会显着增加开销,直到堆栈等超过可用 RAM 并开始被分页'。如果走极端,几乎一切都不好:)
  • 也许你可以发布你的基准代码——我也许也会去!您如何衡量绩效?
  • @MartinJames 你是什么意思?代码在我的答案中。唯一需要注意的是,对于更高数量的线程,GC 甚至会在对 System.gc() 的调用之间启动。
  • 嗯.. 关于 GC 的好点 - 忘记了可能的影响。我会将您的基准转换为 C++(无 GC),然后尝试一下。
【解决方案3】:

为了以最快的速度完成最多的工作:我最好是在需要做更多工作时启动更多线程并让 Java 线程调度程序处理工作分配,还是变得更智能并管理工作负载以保持低于 100% 的 CPU 让我更快?

随着您添加越来越多的线程,上下文切换、内存缓存刷新、内存缓存溢出以及内核和 JVM 线程管理所产生的开销也会增加。当您的线程占用 CPU 时,它们的内核优先级下降到某个最小值,它们将达到时间片最小值。随着越来越多的线程挤占内存,它们会溢出各种内部 CPU 内存缓存。 CPU 需要从较慢的内存中交换作业的可能性更高。在 JVM 内部,有更多的互斥锁本地争用,可能还有一些(可能很小)增量的每线程和对象带宽 GC 开销。根据用户任务的同步程度,更多线程会导致内存刷新和锁争用增加。

对于任何程序和任何架构,都有一个最佳点,线程可以优化利用可用的处理器和 IO 资源,同时限制内核和 JVM 开销。反复寻找最佳位置将需要多次迭代和一些猜测。

我建议使用Executors.newFixedThreadPool(SOME_NUMBER); 并向您提交工作。然后,您可以上下改变线程数进行多次运行,直到根据工作和机器架构找到同时运行的最佳池数。

但请理解,最佳线程数将根据处理器数量和其他可能难以确定的因素而有所不同。如果它们在磁盘或网络 IO 资源上阻塞,则可能需要更多线程。如果他们正在做的工作主要是基于 CPU 的,那么线程会更少。

【讨论】:

  • 嗯?上下文切换仅在操作系统进入中断时发生。如果有大量 CPU 密集型就绪线程,操作系统将围绕运行集进行交换(可能主要在定时器中断之后)。一旦就绪线程集大于内核数,上下文切换开销几乎是恒定的,因为添加了更多线程。
  • 对我来说,上下文切换是在 CPU 级别发生的情况,即当线程产生 IO 或计时器触发并且运行队列中有太多作业时。这会刷新缓存内存 (L1),将运行状态复制到内存,并在下一个作业中交换。我同意存在 JVM/OS 开销下限,但要考虑缓存内存溢出、CPU 优先级惩罚和 JVM 开销会更加复杂。但我应该在回答中更多地谈论限制。
  • 在这些 CPU 密集型作业的情况下,盒子过载的 redy 线程比核心多得多,正如您所描述的,每次更改运行集都会产生开销。这只能发生在硬件中断或系统调用上。 CPU 密集型任务通常不会进行频繁的系统调用,因此会留下中断。如果我们忽略页面错误,CPU 密集型任务也不会做太多的 IO,这样就会留下定时器中断。其频率与就绪线程的数量无关,因此它产生的开销与就绪线程的数量无关。
  • 我同意,尽管时间片窗口不固定。当内核惩罚 CPU 绑定作业时,它确实下降到了一定程度。另外,我不能 100% 确定页面错误可以忽略。我不确定 CPU/内核这些天是否有能力在调度中使用内存位置 - 可能没有。
  • 哦,虽然 CPU 绑定的作业不会进行系统调用,但它们可能会处理内存屏障或锁,这显然也会导致 JVM 中断。
【解决方案4】:

'变得更聪明并管理工作负载以将 CPU 保持在 100% 以下会让我更快吗?'

可能不会。

正如其他人所发布的,如果大多数任务都是 CPU 密集型的,那么 100 个线程对于线程池来说太多了。它不会对典型系统的性能产生太大影响 - 如果过载太多,4 个线程会很糟糕,而 400 线程会很糟糕。

您是如何决定使用 100 个线程的?为什么不是 16 岁?

'线程的数量并不多,比如说多达 100' - 它会有所不同吗?只需在启动时创建 16 个并停止管理它们 - 只需将队列传递给它们并忘记它们。

可怕的想法 - 你没有为每个任务创建一个新线程,是吗?

【讨论】:

  • 为什么是 16?将线程数动态调整为可用处理器的数量确实有意义 - 例如为什么不 (num_processor * 1.5)?
  • num_processor * 1.5 - 很好。 num_processor * 15,差别不大,真的。是的 - 从 sysinfo 中获取处理器的数量,将其加倍并添加您首先想到的数字
【解决方案5】:

您应该保持 100% 的使用率,但使用最少的线程数。 100 个线程看起来太多了。

【讨论】:

  • 为什么? 100 个线程有什么问题,(假设有足够的 RAM 来容纳所有堆栈等而无需连续分页)?
  • @assilias 表明 100 个线程在 Java 中并不是什么大问题。在 C++ 中,2000 个线程并不是什么大问题。
猜你喜欢
  • 2019-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多