FixedThreadPool与CachedThreadPool:两害相权取其轻

110

我有一个程序,会生成一些线程(大约5-150个),来执行一系列任务。最初,我使用了FixedThreadPool,因为这个类似的问题建议它们更适合处理较长时间的任务,并且在我对多线程编程的知识非常有限时,我认为线程的平均寿命(几分钟)是“长时间的”。

然而,最近我添加了生成额外线程的功能,这使得我超过了我设置的线程限制。在这种情况下,增加允许的线程数更好还是切换到CachedThreadPool以避免浪费线程呢?

初步尝试了两种方法,看起来没有差别,所以我倾向于选择CachedThreadPool,只是为了避免浪费。但是,线程的生命周期是否意味着我应该选择一个FixedThreadPool并处理未使用的线程?这个问题似乎表明这些额外的线程不是浪费的,但我希望能够澄清一下。

3个回答

125

CachedThreadPool 似乎适合您的情况,因为直接使用长时间运行的线程没有负面影响。Java文档中关于 CachedThreadPools 适用于短任务只是表明它们特别适合这种情况,并不意味着不能用于长时间运行的任务。

使用 CachedThreadPool 的主要问题在于,它将创建多达Integer.MAX_VALUE个线程,因为如果缓存中不存在未使用的线程,则始终会生成新线程。因此,如果您有长时间运行的任务,则更有可能增加并发线程的数量超过所需,因为此类型的线程池本身不会限制同时执行的数量。根据您描述的用例,这似乎不是一个问题,但这是需要注意的问题。

进一步阐述 CachedThreadPoolFixedThreadPool 之间的区别,Executors.newCachedThreadPoolExecutors.newFixedThreadPool 在至少 open JDK 中都由相同的线程池实现支持,通过 ThreadPoolExecutor 的一个实例来实现,仅仅是线程最小值、最大值、线程结束时间和队列类型不同。

public static ExecutorService newFixedThreadPool(int nThreads) {
     return new ThreadPoolExecutor(nThreads, nThreads,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>());
 }

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                 60L, TimeUnit.SECONDS,
                                 new SynchronousQueue<Runnable>());
}

当您确实希望使用固定数量的线程时,FixedThreadPool具有其优点,因为这样您可以将任意数量的任务提交给执行器服务,同时知道线程数将保持在您指定的级别。如果您明确想要增加线程数,则这不是适当的选择。

然而,这意味着您可能会遇到使用CachedThreadPool的一个问题,即限制正在并发运行的线程数量。 CachedThreadPool不会为您限制它们,因此您可能需要编写自己的代码来确保您不运行过多的线程,通过实例化具有所需行为特性的自己的ThreadPoolExecutor可以相对容易地做到这一点。这实际上取决于您的应用程序设计以及如何将任务提交给执行器服务。


2
“CachedThreadPool 是你应该在这种情况下使用的线程池,因为使用它没有负面后果。” 我不太同意。CachedThreadPool 动态地创建线程,没有上限。大量长时间运行的任务可能会占用所有资源。同时,拥有比理想数量更多的线程可能会导致过多的资源浪费在这些线程的上下文切换上。虽然你在回答的结尾解释了需要自定义限流器,但是回答开头有点误导性。 - Nishit
1
为什么不直接创建一个有界的ThreadPoolExecutor,例如ThreadPoolExecutor(0, maximumPoolSize, 60L, TimeUnit.SECONDS, SynchronousQueue()) - Abhijit Sarkar

60

在高负载应用中,FixedThreadPoolCachedThreadPool都是有害的。

CachedThreadPoolFixedThreadPool更危险。

如果您的应用程序负载高且需要低延迟,则最好摆脱这两个选项,因为存在以下缺点:

  1. 任务队列的无界性:可能导致内存不足或延迟高
  2. 长时间运行的线程将导致CachedThreadPool在线程创建方面失控

既然您知道两者都有害,那么较小的恶并没有任何好处。

推荐使用 ThreadPoolExecutor,它可以对许多参数进行精细控制。 (摘自docs.oracle.com)

  • 将任务队列设置为有界队列,以实现更好的控制
  • 使用正确的RejectionHandler - 使用自己的RejectionHandler或JDK提供的默认处理程序
  • 如果需要在任务完成前/后执行某些操作,请重写beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)
  • 如果需要线程自定义,请覆盖ThreadFactory

  • 如果有人决定使用commonPool会怎么样? - Crosk Cool
    1
    @Ravindra - 你精彩地解释了CachedThreadPool和FixedThreadPool的缺点。这表明你对并发包有深刻的理解。 - Ayaskant
    1
    相同的内容在 https://www.baeldung.com/java-executors-cached-fixed-threadpool 中有详细解释,特别是其中的 https://www.baeldung.com/java-executors-cached-fixed-threadpool#unfortunate-similarities 部分。 - garnet

    5
    我有一个程序,它会生成5到150个线程来执行一些任务。您确定了解操作系统和硬件如何处理线程吗?Java如何将线程映射到操作系统线程,以及如何将其映射到CPU线程等等。我问这个问题是因为在一个JRE内创建150个线程只有在底层有大量CPU核心/线程的情况下才有意义,而这很可能不是事实。根据使用的操作系统和RAM,创建超过n个线程可能会导致JRE由于OOM错误而被终止。因此,您应该真正区分线程和要通过这些线程完成的工作,以及您能够处理多少工作。
    这就是CachedThreadPool的问题:在不能运行这些线程的2个CPU核心的情况下,在线程中排队长时间运行的工作是没有意义的。如果您最终拥有150个已计划的线程,则可能会为Java和OS中使用的调度程序并发处理它们创建大量不必要的开销。除非您的线程一直在等待I / O或类似的操作,否则在只有2个CPU核心的情况下,这是根本不可能的。但即使在那种情况下,许多线程也会产生大量的I / O...
    FixedThreadPool没有这个问题,例如使用2 + n个线程创建,其中n当然是合理低的,因为这样硬件和OS资源使用的开销要少得多,以管理无法运行的线程。

    有时候并没有更好的选择,你可能只有一个CPU核心,但如果你运行的是一个服务器,每个用户请求都会触发一个线程来处理请求,那么就不会有其他合理的选择了,特别是如果你计划在扩大用户基础后扩展服务器。 - Michel Feinstein
    @mFeinstein 如果一个人有选择线程池实现的位置,那么他怎么可能没有选择呢?在你的例子中,只有1个CPU核心,产生更多的线程根本没有任何意义,这与我使用FixedThreadPool的例子完美契合。这也很容易扩展,首先使用一个或两个工作线程,后来根据核心数增加到10或15。 - Thorsten Schöning
    2
    绝大多数Web服务器实现将为每个新的HTTP请求创建一个新线程...它们不会关心机器有多少个实际核心,这使得实现更简单、更易于扩展。这适用于许多其他设计,其中您只想编写一次代码并部署,而不必在更改机器(可能是云实例)时重新编译和重新部署。 - Michel Feinstein
    嗯,我相当确定Tomcat每个请求使用1个线程。虽然我理解你的观点,但我宁愿编写一段在任何机器上都能正常运行并且可以很好地扩展而不需要我的干预的代码...除非它非常需要高性能,那么我会去调整它以适应当前的机器。 - Michel Feinstein
    8
    @ThorstenSchöning,在2核心的计算机上运行50个CPU密集型线程是无益的。在2核心的计算机上运行50个I/O密集型线程会非常有帮助。 - Paul Draper
    显示剩余3条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接