我正在研究Java并发课程的教程。目标是使用线程池并行计算素数。
该设计基于Eratosthenes的Sieve。它有一个n个bool数组,其中n是你要检查的最大整数,数组中的每个元素代表一个整数。 True是素数,false是非素数,数组最初都是真的。
线程池使用固定数量的线程(我们应该试验池中的线程数并观察性能)。
给一个线程一个整数倍来处理。然后线程找到数组中第一个不是线程整数倍数的真元素。然后,线程在线程池上创建一个新线程,该线程被赋予找到的号码。
在形成新线程之后,现有线程继续将数组中所有整数的整数设置为false。
主程序线程以整数'2'启动第一个线程,然后等待所有生成的线程完成。然后它会吐出素数和计算所需的时间。
我遇到的问题是线程池中的线程越多,1个线程最快的线程就越慢。它应该变得更快而不是更慢!
互联网上有关Java线程池的所有内容都会在主线程中创建n个工作线程,然后等待所有线程完成。我使用的方法是递归的,因为worker可以生成更多的工作线程。
我想知道出了什么问题,以及是否可以递归使用Java线程池。
答案 0 :(得分:5)
为以下某些问题添加线程时,您的解决方案可能会运行得更慢:
线程创建开销:创建线程非常昂贵。
处理器争用:如果有多个线程而不是执行它们的处理器,则某些线程将被挂起,等待空闲处理器。结果是每个线程的平均处理速率下降。此外,操作系统需要对线程进行时间分片,这会占用原本用于“真实”工作的时间。
虚拟内存争用:每个线程都需要内存用于其堆栈。如果您的计算机没有足够的物理内存用于工作负载,则每个新的线程堆栈都会增加虚拟内存争用,从而导致分页速度降低
缓存争用:每个线程(可能)将扫描阵列的不同部分,导致内存缓存未命中。这会减慢内存访问速度。
锁争用:如果您的线程都在读取和更新共享阵列并使用synchronized
和一个锁定对象来控制对阵列的访问,那么您可能会遇到锁争用。如果使用单个锁对象,则每个线程将花费大部分时间等待获取锁。最终结果是计算被有效地序列化,并且整体处理速率下降到单个处理器/线程的速率。
前四个问题是多线程所固有的,并且没有真正的解决方案......除了不创建太多线程并重用已经创建的线程。但是,有许多方法可以解决锁争用问题。例如,
最后,递归创建线程可能是一个错误,因为它会使实现线程重用和反锁争用措施变得更加困难。
答案 1 :(得分:3)
您的系统有多少处理器可用?如果#threads> #processors,添加更多线程会减慢像这样的计算绑定任务的速度。
请记住,无论您启动了多少线程,它们仍然共享相同的CPU。 CPU花费在线程之间切换的时间越多,实际工作的时间就越少。
另请注意,与检查素数的成本相比,启动线程的成本很高 - 在启动1个线程所需的时间内,您可能需要进行数百或数千次乘法。
答案 2 :(得分:0)
线程池的关键点是保持一组线程处于活动状态并重新使用它们来处理任务。通常,模式是拥有一个任务队列,并从池中随机选择一个线程来处理它。如果没有空闲线程并且池已满,请等待。
您设计的问题不是线程池要解决的问题,因为您需要线程按顺序运行。如果我在这里错了,请纠正我。
线程#1:将2的倍数设为假
线程#2:找到3,将3的倍数设为假
线程#3:找到5,将5的倍数设置为假
线程#4:找到7,将7的多数设置为假
...
这些线程需要按顺序运行,并且它们交错(运行时如何调度它们)很重要。
例如,如果线程#3在线程#1将“4”设置为false之前开始运行,它将找到“4”并继续重置4的倍数。这最终会做很多额外的工作,尽管最终结果是正确的。
答案 3 :(得分:0)
重新构建程序以提前创建固定的ThreadPoolExecutor。确保调用ThreadPoolExecutor #prestartAllCoreThreads()。让您的main方法为整数2提交任务。每个任务都将提交另一个任务。由于您使用的是线程池,因此您不会创建和终止一堆线程,而是允许相同的线程在新任务可用时承担它们。这将减少总体执行开销。
您应该发现在这种情况下,最佳线程数等于机器上的处理器数(P)。通常情况是最佳线程数是P + 1。这是因为P + 1最大限度地减少了上下文切换的开销,同时最大限度地减少了空闲/阻塞时间的损失。