Executors.newCachedThreadPool()与Executors.newFixedThreadPool()的区别

197
8个回答

233

我认为文档已经很好地解释了这两个函数的区别和用法:

newFixedThreadPool

创建一个线程池来重复使用一定数量的线程,这些线程在共享的无限队列上操作。在任何时候,最多只有 nThreads 线程会活跃地处理任务。如果所有线程都被占用时又提交了额外的任务,则它们将在队列中等待,直到有可用的线程。如果任何线程在关闭之前由于执行期间的故障而终止,则如果需要执行后续任务,将有新的线程取代该线程。线程池中的线程将一直存在,直到它明确停止。

newCachedThreadPool

创建一个线程池,在需要时创建新线程,但会重用先前构建的线程(如果有)。这些池通常会提高执行许多短暂异步任务的程序性能。对 execute 的调用将重用先前构建的线程(如果有)。如果没有现有线程可用,则会创建一个新线程并将其添加到池中。未使用60秒的线程将被终止并从缓存中移除。因此,长时间空闲的池不会消耗任何资源。请注意,可以使用 ThreadPoolExecutor 构造函数创建具有类似属性但不同细节(例如超时参数)的池。

关于资源方面,newFixedThreadPool 将保持所有线程一直运行,直到它们明确终止。在 newCachedThreadPool 中,未使用60秒的线程将被终止并从缓存中移除。

因此,资源消耗将在很大程度上取决于情况。例如,如果您有大量长时间运行的任务,我建议使用FixedThreadPool。 至于CachedThreadPool,文档中指出:“这些池通常会提高执行许多短期异步任务的程序的性能”。


1
是的,我已经阅读了文档。问题是fixedThreadPool在3个线程时会导致内存溢出错误,而cachedPool只会在内部创建一个单线程。当我增加堆大小时,两者的性能都相同。我是否还遗漏了其他东西! - hakish
1
你有为线程池提供任何Threadfactory吗?我猜可能在线程中存储了一些状态而没有被垃圾回收。如果没有,也许你的程序运行非常接近堆限制大小,在创建3个线程时会导致OutOfMemory。此外,如果cachedPool内部仅创建一个线程,则可能表明您的任务正在同步运行。 - bruno conde
正如 @Louis F. 指出的那样,newCachedThreadPool 可能会导致一些严重问题,因为您将所有控制权留给了 线程池,而当 服务 与同一 主机 中的其他服务一起工作时,由于长时间的 CPU 等待,可能会导致其他服务崩溃。因此,在这种情况下,我认为 newFixedThreadPool 更安全。此外,这篇文章澄清了它们之间最显着的区别。 - Hearen

91

为了补充其他答案,我想引用Joshua Bloch在他的书《Effective Java》第二版中的第10章第68条:

"为特定应用程序选择执行器服务可能很棘手。如果您正在编写一个小程序或一个轻负载服务器,使用Executors.newCachedThreadPool通常是一个不错的选择,因为它不需要配置并且通常“做正确的事情”。但是,对于负载繁重的生产服务器来说,缓存线程池并不是一个好的选择

缓存线程池中,提交的任务不会排队,而是立即移交给一个线程进行执行。如果没有可用的线程,则会创建一个新线程。如果服务器过载以至于所有CPU都被完全利用,并且更多任务到达,则会创建更多线程,这只会使问题变得更糟。

因此,在负载繁重的生产服务器中,最好使用Executors.newFixedThreadPool来获得具有固定线程数的线程池,或直接使用ThreadPoolExecutor类进行最大控制。"


27

如果您在grepcode中查看代码,您会发现它们在内部调用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>());
}

1
一个带有合理上限和5-10分钟空闲回收的缓存线程执行器对于大多数情况来说是完美的。 - Agoston Horvath

26

ThreadPoolExecutor类是许多Executors工厂方法返回的执行器的基本实现。因此,让我们从ThreadPoolExecutor的角度来看待FixedCached线程池。

ThreadPoolExecutor

该类的主构造函数如下所示:

public ThreadPoolExecutor(
                  int corePoolSize,
                  int maximumPoolSize,
                  long keepAliveTime,
                  TimeUnit unit,
                  BlockingQueue<Runnable> workQueue,
                  ThreadFactory threadFactory,
                  RejectedExecutionHandler handler
)

核心池大小

corePoolSize 确定目标线程池的最小大小。即使没有任务需要执行,实现也会维护该大小的线程池。

最大池大小

maximumPoolSize 是可以同时活动的线程的最大数量。

当线程池增长并变得比 corePoolSize 阈值更大时,执行程序可以终止空闲线程并再次达到 corePoolSize。 如果 allowCoreThreadTimeOut 为 true,则执行程序甚至可以终止核心池线程,如果它们空闲时间超过 keepAliveTime 阈值。

因此,归根结底,如果线程保持空闲状态超过 keepAliveTime 阈值,它们可能会被终止,因为没有对它们的需求。

排队

当新任务进来时,所有核心线程都被占用会发生什么?新任务将在 BlockingQueue<Runnable> 实例中排队。当线程变为空闲时,可以处理其中一个排队的任务。

Java 中有不同的 BlockingQueue 接口实现,因此我们可以实现不同的排队方法,如:

  1. 有界队列: 新任务将排队在一个有界任务队列中。

  2. 无界队列: 新任务将排队在一个无界任务队列中。因此,该队列可以根据堆大小的限制而增长。

  3. 同步移交: 我们也可以使用 SynchronousQueue 来排队新任务。在这种情况下,当排队新任务时,另一个线程必须已经在等待该任务。

工作提交

以下是 ThreadPoolExecutor 如何执行新任务:

  1. 如果正在运行的线程少于 corePoolSize,则尝试启动一个新线程,并以给定任务作为其第一个作业。
  2. 否则,它会尝试使用 BlockingQueue#offer 方法将新任务排队。如果队列已满,则 offer 方法不会阻塞并立即返回 false
  3. 如果无法排队新任务(即 offer 返回 false),则尝试向线程池添加一个新线程,并以此任务作为其第一个作业。
  4. 如果无法添加新线程,则执行程序已关闭或已饱和。无论哪种方式,都将使用提供的 RejectedExecutionHandler 拒绝新任务。

固定线程池和缓存线程池之间的主要区别在于这三个因素:

  • 核心池大小
  • 最大池大小
  • 排队
+-----------+-----------+-------------------+---------------------------------+
| 池类型 | 核心大小 |    最大大小   |         排队策略        |
+-----------+-----------+-------------------+---------------------------------+
|   固定   | n (固定) |     n (固定)     | 无限制的 `LinkedBlockingQueue` |
+-----------+-----------+-------------------+---------------------------------+
|   缓存  |     0     | Integer.MAX_VALUE |        `SynchronousQueue`       |
+-----------+-----------+-------------------+---------------------------------+


固定大小线程池


以下是Excutors.newFixedThreadPool(n)的工作原理:

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

正如您所看到的:

  • 线程池大小是固定的。
  • 如果需求量很高,它不会增长。
  • 如果线程空闲了相当长的时间,它也不会缩小。
  • 假设所有这些线程都被一些长时间运行的任务占用,到达率仍然相当高。由于执行程序使用无界队列,它可能会消耗堆的大部分。不幸的是,我们可能会遇到OutOfMemoryError。

我应该在何时使用其中之一?哪种策略在资源利用方面更好?

当我们要限制并发任务数量以进行资源管理时,固定大小的线程池似乎是一个不错的选择。

例如,如果我们要使用执行程序处理Web服务器请求,则固定执行程序可以更合理地处理请求爆发。

为了更好的资源管理,强烈建议创建一个自定义ThreadPoolExecutor,其中包含有限的BlockingQueue<T>实现以及合理的RejectedExecutionHandler。


缓存线程池


这是Executors.newCachedThreadPool()的工作方式: {{解释}}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

如您所见:

  • 线程池可以从零个线程增长到Integer.MAX_VALUE。实际上,线程池是无限制的。
  • 如果任何线程闲置超过1分钟,则可能被终止。因此,如果线程保持太多闲置,则池可能会缩小。
  • 如果所有分配的线程都被占用,而新任务进来,则创建一个新线程,因为当没有人在另一端接受它时,向SynchronousQueue提供新任务总是失败的!

什么情况下应使用其中之一?哪种策略在资源利用方面更好?

当您有大量可预测的短期运行任务时使用它。


2
感谢您的帖子。我希望这篇文章能够获得更多的投票,以便它可以上升 ^^^ 在做决定时了解机制背后总是更好。 - hqt
很好的解释。你能给一些短期运行任务的例子吗?我想网络/REST调用API或数据库检索不属于它们? - linuxNoob

16

如果您不担心无限队列的Callable/Runnable任务,可以使用其中之一。正如bruno所建议的那样,我也更喜欢newFixedThreadPool而不是这两个中的任何一个。

但是,无限队列大小总是很危险的。如果系统中出现了一些意外的动荡,当前线程被卡住,队列大小将增加,并可能导致内存溢出错误或系统性能下降。

但是ThreadPoolExecutor相对于newFixedThreadPoolnewCachedThreadPool提供了更灵活的功能。它提供了对各种属性的更细粒度的控制。

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)

优点:

  1. 您可以完全控制BlockingQueue的大小。与前两个选项不同,它不是无限制的。当系统出现意外波动时,由于挂起的Callable/Runnable任务堆积过多而导致内存溢出错误的情况将不会发生。

  2. 您可以实现自定义的拒绝处理策略或使用以下策略之一:

    1. 在默认的ThreadPoolExecutor.AbortPolicy中,处理程序在拒绝时抛出运行时RejectedExecutionException异常。
    1. ThreadPoolExecutor.CallerRunsPolicy中,调用execute方法的线程本身运行任务。这提供了一个简单的反馈控制机制,可以减缓提交新任务的速率。
    1. ThreadPoolExecutor.DiscardPolicy中,无法执行的任务将被简单地丢弃。
    1. ThreadPoolExecutor.DiscardOldestPolicy中,如果执行器没有关闭,则删除工作队列头部的任务,然后重试执行(可能会再次失败,从而导致重复执行)。
  3. 您可以为以下用例实现自定义线程工厂:

    通过提供不同的ThreadFactory,您可以更改线程的名称、线程组、优先级、守护进程状态等。


14

没错,Executors.newCachedThreadPool()不是为服务多个客户端和并发请求的服务器代码而设计的最佳选择。

为什么呢?主要有以下两个(相关)问题:

  1. 它没有限制,这意味着任何人都可以通过向服务注入更多工作来使您的JVM崩溃(DoS攻击)。线程消耗了相当大的内存,并且根据其正在进行的工作增加内存消耗,因此很容易以这种方式使服务器崩溃(除非您还有其他断路器)。

  2. 未受限制的问题还被执行程序前面的SynchronousQueue所加剧,这意味着任务提供者和线程池之间有直接交接。如果所有现有线程都忙,则每个新任务都会创建一个新线程。这通常不适用于服务器代码的策略。当CPU饱和时,现有任务需要更长时间才能完成。然而,越来越多的任务正在提交,越来越多的线程被创建,因此任务需要越来越长的时间才能完成。当CPU饱和时,服务器绝对不需要更多的线程。

下面是我的建议:

使用具有设置最大线程数的固定大小线程池 Executors.newFixedThreadPoolThreadPoolExecutor.


5

根据Javadoc的说明,只有在处理短暂异步任务时才应使用newCachedThreadPool,如果您提交需要较长时间处理的任务,则会创建过多的线程。如果您以更快的速度向newCachedThreadPool提交长时间运行的任务,则可能会达到100%的CPU使用率(http://rashcoder.com/be-careful-while-using-executors-newcachedthreadpool/)。


1
我进行了一些快速测试并得出以下结果:
1)如果使用SynchronousQueue:
当线程达到最大大小后,任何新的工作都将被拒绝,并显示以下异常。
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3fee733d rejected from java.util.concurrent.ThreadPoolExecutor@5acf9800[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
2)如果使用LinkedBlockingQueue:
线程从最小大小到最大大小永远不会增加,这意味着线程池的大小固定为最小大小。

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