为什么通过newCachedThreadPool evil创建了ExecutorService?

Paul Tyma 演讲有这样一句:

Executors.newCacheThreadPool恶,死死死

为什么这是邪恶的?

我会冒险猜测:是因为线程的数量会以无限的方式增长。 因此,如果达到了JVM的最大线程数,那么被斜线扫描的服务器可能会死掉?

Executors.newCacheThreadPool()的问题在于执行程序将根据需要创建和启动尽可能多的线程来执行提交给它的任务。 虽然已完成的线程被释放(阈值可配置)这一事实可以缓解这种情况,但这确实会导致严重的资源匮乏,甚至导致JVM(或某些设计糟糕的操作系统)崩溃。

(这是保罗)

幻灯片的目的是(除了具有滑稽的措辞),正如您所提到的那样,线程池在不受限制地创建新线程的情况下增长。

线程池固有地表示系统内的队列和传输工作点。 也就是说,有些事情正在为它的工作做准备(而且它也可能在其他地方提供工作)。 如果线程池开始增长,因为它无法跟上需求。

一般来说,这很好,因为计算机资源是有限的,并且该队列是为处理突发的工作而构建的。 但是,该线程池无法控制是否能够推动瓶颈。

例如,在服务器方案中,一些线程可能正在接受套接字并将线程池交给客户端进行处理。 如果该线程池开始失控 – 系统应该停止接受新客户端(实际上,“接受者”线程然后经常暂时跳入线程池以帮助处理客户端)。

如果使用具有无界输入队列的固定线程池,则效果类似。 任何时候你认为队列的情况失控 – 你意识到了问题。

IIRC,Matt Welsh的开创性SEDA服务器(它们是异步的)创建了线程池,根据服务器特性修改了它们的大小。

停止接受新客户端的想法听起来很糟糕,直到您意识到替代方案是一个没有处理客户端的残缺系统。 (再次,理解计算机是有限的 – 即使是经过优化调整的系统也有限制)

顺便说一句,JVM将线程限制为16k(通常)或32k线程,具体取决于JVM。 但是如果你受CPU限制,那么这个限制并不是很相关 – 在CPU绑定系统上启动另一个线程会适得其反。

我很乐意在4或5千个线程上运行系统。 但是接近16k的限制事情往往会陷入困境(这限制JVM强制执行 – 我们在linux C ++中有更多的线程),即使没有CPU限制。

它有几个问题。 线程方面的无限增长是一个明显的问题 – 如果你有cpu绑定任务然后允许比可用的CPU更多的运行它们只是创建调度程序开销,你的线程上下文切换到处都没有实际进展。 如果你的任务是IO绑定的,虽然事情变得更加微妙。 知道如何调整在网络或文件IO上等待的线程池的大小要困难得多,并且很大程度上取决于这些IO事件的延迟。 更高的延迟意味着您需要(并且可以支持)更multithreading。

缓存的线程池继续添加新线程,因为任务生成速率超过了执行速率。 这有一些小的障碍(比如序列化新线程ID创建的锁)但是这种未绑定的增长可能会导致内存不足错误。

缓存线程池的另一个大问题是它对任务生产者线程来说可能很慢。 该池配置了SynchronousQueue,用于提供任务。 这个队列实现基本上没有大小,只有当生产者有匹配的消费者时才有效(当另一个提供时有一个线程轮询)。 Java6中的实际实现得到了显着改进,但对于生产者来说仍然相对较慢,特别是当它失败时(因为生产者负责创建一个新的线程来添加到池中)。 通常,生产者线程更理想的是简单地将任务放在实际队列上并继续。

问题是,没有人拥有一个拥有一小组核心线程的池,当它们都忙时会创建新的线程,最多可达到某个最大值,然后将后续任务排入队列。 修复的线程池似乎承诺这一点,但是当底层队列拒绝更多任务(它已满)时,它们只会开始添加更multithreading。 LinkedBlockingQueue永远不会变满,因此这些池永远不会超出核心大小。 ArrayBlockingQueue具有容量,但由于它只在达到容量时增加池,因此在它已经是一个大问题之前不会降低生产率。 目前,该解决方案需要使用良好的拒绝执行策略,例如调用者运行,但需要一些小心。

开发人员会看到缓存的线程池并盲目地使用它而不会真正考虑后果。