使用线程池你需要注意这几点( 二 )

除了建议手动声明线程池以外,我还建议用一些监控手段来观察线程池的状态 。线程池这个组件往往会表现得任劳任怨、默默无闻,除非是出现了拒绝策略,否则压力再大都不会抛出一个异常 。如果我们能提前观察到线程池队列的积压,或者线程数量的快速膨胀,往往可以提早发现并解决问题 。
线程池线程管理策略详解在之前的 Demo 中,我们用一个 printStats 方法实现了最简陋的监控,每秒输出一次线程池的基本内部信息,包括线程数、活跃线程数、完成了多少任务,以及队列中还有多少积压任务等信息:

使用线程池你需要注意这几点

文章插图
   接下来,我们就利用这个方法来观察一下线程池的基本特性吧 。
首先,自定义一个线程池 。这个线程池具有 2 个核心线程、5 个最大线程、使用容量为 10 的 ArrayBlockingQueue 阻塞队列作为工作队列,使用默认的 AbortPolicy 拒绝策略,也就是任务添加到线程池失败会抛出 RejectedExecutionException 。此外,我们借助了 Jodd 类库的 ThreadFactoryBuilder 方法来构造一个线程工厂,实现线程池线程的自定义命名 。
然后,我们写一段测试代码来观察线程池管理线程的策略 。测试代码的逻辑为每次间隔 1 秒向线程池提交任务,循环 20 次,每个任务需要 10 秒才能执行完成,代码如下:
使用线程池你需要注意这几点

文章插图
   60 秒后页面输出了 17,有 3 次提交失败了:
使用线程池你需要注意这几点

文章插图
   并且日志中也出现了 3 次类似的错误信息:
使用线程池你需要注意这几点

文章插图
   我们把 printStats 方法打印出的日志绘制成图表,得出如下曲线:
使用线程池你需要注意这几点

文章插图
   至此,我们可以总结出线程池默认的工作行为:
  1. 不会初始化 corePoolSize 个线程,有任务来了才创建工作线程;
  2. 当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中;
  3. 当工作队列满了后扩容线程池,一直到线程个数达到 maximumPoolSize 为止;
  4. 如果队列已满且达到了最大线程后还有任务进来,按照拒绝策略处理;
  5. 当线程数大于核心线程数时,线程等待 keepAliveTime 后还是没有任务需要处理的话,收缩线程到核心线程数 。
了解这个策略,有助于我们根据实际的容量规划需求,为线程池设置合适的初始化参数 。当然,我们也可以通过一些手段来改变这些默认工作行为,比如:
  1. 声明线程池后立即调用 prestartAllCoreThreads 方法,来启动所有核心线程;
  2. 传入 true 给 allowCoreThreadTimeOut 方法,来让线程池在空闲的时候同样回收核心线程 。
不知道你有没有想过:Java 线程池是先用工作队列来存放来不及处理的任务,满了之后再扩容线程池 。当我们的工作队列设置得很大时,最大线程数这个参数显得没有意义,因为队列很难满,或者到满的时候再去扩容线程池已经于事无补了 。
那么,我们有没有办法让线程池更激进一点,优先开启更多的线程,而把队列当成一个后备方案呢?比如我们这个例子,任务执行得很慢,需要 10 秒,如果线程池可以优先扩容到 5 个最大线程,那么这些任务最终都可以完成,而不会因为线程池扩容过晚导致慢任务来不及处理 。
限于篇幅,这里我只给你一个大致思路:
  1. 由于线程池在工作队列满了无法入队的情况下会扩容线程池,那么我们是否可以重写队列的 offer 方法,造成这个队列已满的假象呢?
  2. 由于我们 Hack 了队列,在达到了最大线程后势必会触发拒绝策略,那么能否实现一个自定义的拒绝策略处理程序,这个时候再把任务真正插入队列呢?
接下来,就请你动手试试看如何实现这样一个“弹性”线程池吧 。Tomcat 线程池也实现了类似的效果,可供你借鉴 。
务必确认清楚线程池本身是不是复用的不久之前我遇到了这样一个事故:某项目生产环境时不时有报警提示线程数过多,超过 2000 个,收到报警后查看监控发现,瞬时线程数比较多但过一会儿又会降下来,线程数抖动很厉害,而应用的访问量变化不大 。
为了定位问题,我们在线程数比较高的时候进行线程栈抓取,抓取后发现内存中有 1000 多个自定义线程池 。一般而言,线程池肯定是复用的,有 5 个以内的线程池都可以认为正常,而 1000 多个线程池肯定不正常 。


推荐阅读