前阵子在v2ex上看到这篇帖子讨论这个问题,有意思的是这个如此基础的问题在Javaer的世界里并没有广泛的共识,下面的回答也是七嘴八舌的,刚好在《Java Performace》上看到对这个问题的解释,尝试总结一下。
原因
书中对线程池的解释基于以下几点前提:
- 如果CPU已经跑满,增加线程并不能提高系统吞吐,更多的线程切换开销反而会降低性能
- 核心线程用尽之后CPU负载如何线程池并不清楚,这取决于核心线程数的大小以及当前任务的性质(CPU密集还是IO密集)
- 线程池不一定要用满所有CPU,有时线程数本来就是一种CPU资源限制的手段
理想情况下线程池中Runnable
的线程数应该刚好等于CPU核心数,如果任务都是CPU密集型,那么线程数就等于核心数;如果是IO密集型,那么就需要计算CPU耗时和IO耗时的比例来调整核心线程数。现实中的情况往往更加复杂,任务可能有CPU密集的也有IO密集的,IO密集的耗时比例也不尽相同。调整核心线程数、最大线程数和队列长度来获得理想的系统吞吐和请求耗时这是开发者的责任,线程池提供机制但无法解决这个问题。
线程池的逻辑或者说约定:
- 假设需要核心线程数的线程来使CPU达到饱和。如果当前线程数没有达到核心线程数,线程池总是新建线程来执行任务,即使现有线程有空闲的。
- 如果现有线程数达到核心线程数而队列未满,将任务推进队列。此时假设CPU资源已经饱和,任务需要等待CPU资源释放,再增加线程只会降低性能。
- 如果连队列都已经满了,继续创建线程直至最大线程数。此时新提交的任务依然排队,从队头取一个任务交给新创建的线程。此时线程池认为系统已经过载,创建新线程属于试试能不能抢救一下。
- 最大线程数都达到了,再有新任务提交直接调用拒绝策略。
如上,可见设置的关键是核心线程数,核心线程数应该尽量使CPU饱和(或者达到我们期望的负载)但又不会产生过多的上下文切换。考虑到任务的复杂性,这个参数确实只能通过压力测试来得到。
其它的一些模式
ThreadPoolExecutor
的配置是非常灵活的,可能通过调整参数使得线程池采取一些别的行为。
令核心线程数等于最大线程数,就可以取得原贴所期望的,可能也是大部分人所期望的,到达最大线程数后再排队。不过核心线程是不会被回收的,如果确实需要回收可以设置allowCoreThreadTimeOut
。如果使用无容量限制的队列如 LinkedBlockedingQueue
那么行为就和Executors#newFixedThreadPool
相同。
令队列长度等于0,最大线程数等于无限(Integer#MAX_VALUE
,等效于无限),此时所有任务都会直接提交给线程,没有空闲的就新建,不会有任务排队。此时等效于Executors#newCachedThreadPool
。
上面两种方式多多少少都有点问题,这也是为什么不建议通过Executors
来创建线程池。