为什么通过newCachedThreadPool创建ExecutorService是有害的?

8
保罗·泰马的演示文稿中有这样一句话:Executors.newCacheThreadPool evil, die die die。为什么会是邪恶的呢?我猜测可能是因为线程数量会无限增长,因此如果达到JVM的最大线程数,一个被大量访问的服务器可能会崩溃。

1
我使用了一个缓存池,它让我的Windows Vista系统崩溃得如此严重,以至于我不得不强制关机。任务管理器无法响应。 - Peter Lawrey
因为同样的原因来到这里 :) - Eugen
3个回答

17

(这是Paul)

除了幽默的措辞外,幻灯片的意图是,正如您提到的那样,线程池无限增长并创建新线程。

线程池固有地表示系统内工作的队列和传输点。也就是说,有些东西正在为它提供工作(它也可能在其他地方提供工作)。如果线程池开始增长,那是因为它无法满足需求。

一般来说,由于计算机资源是有限的,队列是建立用来处理工作的突发情况的。然而,该线程池不能使你能够控制瓶颈的推进。

例如,在服务器场景中,几个线程可能正在接受套接字并将客户端交给线程池进行处理。如果线程池开始失控,则系统应停止接受新客户端(实际上,“接受器”线程通常暂时跳入线程池以帮助处理客户机)。

如果您使用具有无限输入队列的固定线程池,则效果类似。任何时候考虑队列失控的情况 - 您就会意识到问题所在。

我记得Matt Welsh的开创性SEDA服务器(异步)创建了可以根据服务器特性修改其大小的线程池。

停止接受新客户端的想法听起来很糟糕,直到您意识到另一种选择是处理零个客户端的瘫痪系统。 (再次强调,计算机是有限的-即使是经过优化的系统也有极限)

顺便说一下,JVM将线程限制为16k(通常)或32k线程,具体取决于JVM。但是,如果您受制于CPU,则该限制并不是很相关-在CPU限制的系统上启动另一个线程是适得其反的。

我曾经快乐地运行过拥有4或5千个线程的系统。但是靠近16k的限制时,事情往往会变得混乱不堪(这是JVM强制执行的-即使在非CPU限制的情况下,在Linux C ++中我们有更多的线程)。


1
鉴于这是原始引用的作者并且他提供了最全面的回答,我认为这应该是被接受的答案。 - KomodoDave

8

Executors.newCacheThreadPool() 方法的问题在于,执行器将创建并启动尽可能多的线程来执行提交给它的任务。虽然已完成的线程会被释放(阈值是可配置的),但这确实可能导致严重的资源匮乏,甚至崩溃JVM(或一些设计不良的操作系统)。


4
有一些问题需要注意。线程无限增长是一个明显的问题-如果您有CPU绑定任务,那么允许比可用CPU多得多的线程运行它们只会创建调度程序开销,因为您的线程在上下文切换时到处移动,但实际上没有进展。但如果您的任务是IO绑定的,则情况会更加微妙。知道如何调整等待网络或文件IO的线程池大小要困难得多,并且很大程度上取决于这些IO事件的延迟时间。更高的延迟意味着您需要(并且可以支持)更多线程。
缓存线程池在任务生产速率超过执行速率时继续添加新线程。虽然存在一些小障碍(例如序列化新线程ID创建的锁),但此无界增长可能会导致内存不足错误。
缓存线程池的另一个大问题是,对于任务生产者线程来说可能会变慢。池配置了SynchronousQueue,以便将任务提供给它。此队列实现基本上没有大小,仅在生产者和消费者匹配时才起作用(当另一个线程正在提供时,有一个线程在轮询)。实际实现在Java6中得到了显着改进,但对于生产者来说仍然相对较慢,特别是当它失败时(因为生产者随后负责创建一个新线程添加到池中)。通常,对于生产者线程来说,最理想的情况是将任务放在实际队列上并继续。
问题是,没有人拥有一个具有小核心线程集的池,当所有线程都忙碌时,会创建新线程以达到某个最大值,然后排队执行后续任务。固定线程池似乎承诺了这一点,但只有在基础队列拒绝更多任务(已满)时才开始添加更多线程。 LinkedBlockingQueue永远不会满,因此这些池永远不会超出核心大小。 ArrayBlockingQueue具有容量,但由于它仅在达到容量时才增长池,因此在它已经成为一个大问题之前,它并不能缓解生产率。目前的解决方案需要使用良好的拒绝执行策略,例如调用者运行,但需要一些注意。开发人员看到缓存线程池并盲目使用它,而没有真正考虑后果。

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