在项目代码里,我们没有搜到声明线程池的地方,搜索 execute 关键字后定位到,原来是业务代码调用了一个类库来获得线程池,类似如下的业务代码:调用 ThreadPoolHelper 的 getThreadPool 方法来获得线程池,然后提交数个任务到线程池处理,看不出什么异常 。
文章插图
但是,来到 ThreadPoolHelper 的实现让人大跌眼镜,getThreadPool 方法居然是每次都使用 Executors.newCachedThreadPool 来创建一个线程池 。
文章插图
通过上一小节的学习,我们可以想到 newCachedThreadPool 会在需要时创建必要多的线程,业务代码的一次业务操作会向线程池提交多个慢任务,这样执行一次业务操作就会开启多个线程 。如果业务操作并发量较大的话,的确有可能一下子开启几千个线程 。
那为什么我们能在监控中看到线程数量会下降,而不会撑爆内存呢?
回到 newCachedThreadPool 的定义就会发现,它的核心线程数是 0,而 keepAliveTime 是 60 秒,也就是在 60 秒之后所有的线程都是可以回收的 。好吧,就因为这个特性,我们的业务程序死得没太难看 。
要修复这个 Bug 也很简单,使用一个静态字段来存放线程池的引用,返回线程池的代码直接返回这个静态字段即可 。这里一定要记得我们的最佳实践,手动创建线程池 。修复后的 ThreadPoolHelper 类如下:
文章插图
需要仔细斟酌线程池的混用策略线程池的意义在于复用,那这是不是意味着程序应该始终使用一个线程池呢?
当然不是 。这要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列:
- 对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列 。
- 而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲 。
经排查发现,业务代码使用的线程池,还被一个后台的文件批处理任务用到了 。
或许是够用就好的原则,这个线程池只有 2 个核心线程,最大线程也是 2,使用了容量为 100 的 ArrayBlockingQueue 作为工作队列,使用了 CallerRunsPolicy 拒绝策略:
文章插图
这里,我们模拟一下文件批处理的代码,在程序启动后通过一个线程开启死循环逻辑,不断向线程池提交任务,任务的逻辑是向一个文件中写入大量的数据:
文章插图
可以想象到,这个线程池中的 2 个线程任务是相当重的 。通过 printStats 方法打印出的日志,我们观察下线程池的负担:
文章插图
可以看到,线程池的 2 个线程始终处于活跃状态,队列也基本处于打满状态 。因为开启了 CallerRunsPolicy 拒绝处理策略,所以当线程满载队列也满的情况下,任务会在提交任务的线程,或者说调用 execute 方法的线程执行,也就是说不能认为提交到线程池的任务就一定是异步处理的 。如果使用了 CallerRunsPolicy 策略,那么有可能异步任务变为同步执行 。从日志的第四行也可以看到这点 。这也是这个拒绝策略比较特别的原因 。
不知道写代码的同学为什么设置这个策略,或许是测试时发现线程池因为任务处理不过来出现了异常,而又不希望线程池丢弃任务,所以最终选择了这样的拒绝策略 。不管怎样,这些日志足以说明线程池是饱和状态 。
可以想象到,业务代码复用这样的线程池来做内存计算,命运一定是悲惨的 。我们写一段代码测试下,向线程池提交一个简单的任务,这个任务只是休眠 10 毫秒没有其他逻辑:
推荐阅读
- 淘宝促销宝是干什么的 淘宝促销宝怎么使用
- 车内a/c是什么意思?意思很简单,使用技巧也很简单
- 淘宝选词工具 选词助手免费使用的吗
- 店铺宝满就送优惠券怎么使用 淘宝如何设置赠品
- 使用环保袋的好处
- 手机千牛上的单品宝在哪里? 手机千牛可以使用单品宝吗
- 淘宝店极速推有用吗 淘宝极速推怎么使用
- 淘宝免费选词工具 选词助手免费使用的吗
- 淘宝店侦探手机版下载 店侦探怎么使用
- 超级推荐新品推广怎么使用