【问题标题】:Recursively adding threads to a Java thread pool递归地将线程添加到 Java 线程池
【发布时间】:2011-02-06 09:51:23
【问题描述】:

我正在为我的 Java 并发课程编写教程。目标是使用线程池并行计算素数。

该设计基于埃拉托色尼筛法。它有一个包含 n 个布尔值的数组,其中 n 是您要检查的最大整数,数组中的每个元素代表一个整数。 True 为素数,false 为非素数,数组初始全部为真。

线程池使用固定数量的线程(我们应该试验池中的线程数量并观察性能)。

一个线程被赋予一个整数倍来处理。然后线程在数组中找到第一个不是线程整数倍数的真正元素。然后该线程在线程池上创建一个新线程,并给定找到的编号。

形成新线程后,现有线程继续将数组中其整数的所有倍数设置为false。

主程序线程以整数“2”启动第一个线程,然后等待所有派生线程完成。然后它会吐出素数和计算时间。

我遇到的问题是线程池中的线程越多,速度越慢,1 个线程最快。它应该变得更快而不是更慢!

互联网上所有关于 Java 线程池的东西都会创建 n 个工作线程作为主线程,然后等待所有线程完成。我使用的方法是递归的,因为一个工作线程可以产生更多的工作线程。

我想知道出了什么问题,以及是否可以递归使用 Java 线程池。

【问题讨论】:

  • 继续使用线程方法,这是一种学习体验,完成后您将了解很多有关线程的知识。谁在乎埃拉托色尼筛?很多专业程序员永远不懂这个页面上的知识。请记住,如果一个女人9个月可以生孩子,并不意味着9个可以在1个月内完成!!!

标签: java concurrency multithreading threadpool


【解决方案1】:

您的系统上有多少个处理器可用?如果#threads > #processors,添加更多线程会减慢此类计算密集型任务的速度。

请记住,无论您启动多少线程,它们仍然都共享相同的 CPU。 CPU 在线程之间切换的时间越多,它可以做实际工作的时间就越少。

另请注意,与检查素数的成本相比,启动线程的成本很高 - 您可能会在启动 1 个线程所需的时间内进行数百或数千次乘法运算。

【讨论】:

  • 我当前的计算机有两个内核,但代码最初是在具有 4 个内核(8 个虚拟)的 Intel Core i7 上测试的,并且有类似的问题。即 1 个线程 = 1s。 4 个线程 = 20 秒。
【解决方案2】:

线程池的关键在于保持一组线程处于活动状态并重新使用它们来处理任务。通常模式是有一个任务队列并从池中随机选择一个线程来处理它。如果没有空闲线程且池已满,则等待。

你设计的问题不是一个线程池来解决的好问题,因为你需要线程按顺序运行。如果我在这里错了,请纠正我。

线程 #1:将 2 的倍数设置为 false

线程#2:找到3,设置3的倍数为假

线程 #3:找到 5,将 5 的倍数设置为 false

线程#4:找到7,设置7的倍数为假

....

这些线程需要按顺序运行并且它们是交错的(运行时如何调度它们)很重要。

例如,如果线程#3 在线程#1 将“4”设置为false 之前开始运行,它将找到“4”并继续重置4 的倍数。尽管最终结果是正确的,但这最终会做很多额外的工作。

【讨论】:

  • 线程 5 永远不会“找到” 4 - 它的唯一目的是删除所有 5 的倍数。一旦所有工作线程完成删除非素数,主线程将“找到”剩余的数字。
  • 如果线程#3 需要在它完成之前等待启动的线程,我们不需要多个线程。这里的问题是线程 #3 不需要等待线程 #1 删除 all 的 2 的倍数,但它需要等待线程 #1 在开始搜索之前删除“足够”的 2 的倍数.
【解决方案3】:

由于以下一些问题添加了线程,您的解决方案可能会运行得更慢:

  • 线程创建开销:创建线程很昂贵。

  • 处理器争用:如果线程数多于执行它们的处理器数,一些线程将暂停等待空闲处理器。结果是每个线程的平均处理速率下降。此外,操作系统需要对线程进行时间切片,这会占用原本用于“真正”工作的时间。

  • 虚拟内存争用:每个线程的堆栈都需要内存。如果您的机器没有足够的物理内存来处理工作负载,每个新线程堆栈都会增加虚拟内存争用,从而导致分页,从而减慢速度

  • 缓存争用:每个线程将(可能)扫描数组的不同部分,导致内存缓存未命中。这会减慢内存访问速度。

  • 锁争用:如果您的线程都在读取和更新共享数组并使用synchronized 和一个锁对象来控制对数组的访问,那么您可能会遇到锁争用。如果使用单个锁对象,每个线程将花费大部分时间等待获取锁。最终结果是计算被有效地序列化,整体处理速率下降到单处理器/线程的速率。

前四个问题是多线程固有的,并且没有真正的解决方案……除了不要创建太多线程并重用您已经创建的线程。但是,有许多方法可以解决锁争用问题。例如,

  • 重新编码应用程序,以便每个线程扫描多个整数,但在其自己的数组部分。这将消除数组上的锁争用,但您需要一种方法来告诉每个线程该做什么,并且需要在设计时考虑到争用。
  • 为数组的不同区域创建一个锁数组,并让线程根据它们正在操作的数组区域选择要使用的锁。您仍然会遇到争用,但平均而言,您应该会遇到更少的争用。
  • 设计和实施无锁解决方案。这需要深入了解 Java 内存模型。并且很难证明/证明无锁解决方案不包含细微的并发缺陷。

最后,递归线程的创建可能是一个错误,因为它会使线程重用和抗锁争用措施的实现变得更加困难。

【讨论】:

  • 简而言之,您确实必须测试添加线程是否会提高性能,不要假设增加复杂性会带来更好的解决方案,因为通常情况下并没有。
  • @Peter - 我想说练习的重点是您必须以正确的方式使用线程来让应用程序的性能随着您的添加而扩大处理器。
  • 我真的觉得给这篇文章投一票是不够的,这是对多线程可能缺点的一个很好的总结。
【解决方案4】:

重构你的程序,提前创建一个固定的ThreadPoolExecutor。确保调用 ThreadPoolExecutor#prestartAllCoreThreads()。让您的 main 方法为整数 2 提交一个任务。每个任务将提交另一个任务。由于您使用的是线程池,因此您不会创建和终止一堆线程,而是允许相同的线程在它们可用时执行新任务。这将减少整体执行开销。

您应该发现,在这种情况下,最佳线程数等于机器上的处理器数 (P)。通常情况下,最佳线程数是 P+1。这是因为 P+1 最大限度地减少了上下文切换的开销,同时也最大限度地减少了空闲/阻塞时间的损失。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-21
    • 2011-11-16
    • 1970-01-01
    • 1970-01-01
    • 2013-04-19
    • 2011-11-23
    • 2011-06-01
    相关资源
    最近更新 更多