本文 首发于 🍀 永浩转载 请注明 来源

9、【对线面试官】线程池

今天来聊聊线程池呗,你对Java线程池了解多少?

或者换个问法:为什么需要线程池?

  1. JVM在HotSpot的线程模型下,Java线程会一对一映射为内核线程
  2. 这意味着,在Java中每次创建以及回收线程都会去内核创建以及回收
  3. 这就有可能导致:创建和销毁线程所花费的时间和资源可能比处理的任务花费的时间和资源要更多
  4. 线程池的出现是为了提高线程的复用性以及固定线程的数量!!!

你在项目中用到了线程池吗?

  1. 嗯,用到的。我先说下背景吧
  2. 我所负责的项目是消息管理平台,提供其中一个功能就是:运营会圈定人群,然后群发消息
  3. 主要流程大致就是:创建模板-》定时-》群发消息-》用户收到消息
  4. 运营圈定的人群实际上在模板上只是一个ID,我这边要通过ID去获取到HDFS文件
  5. 对HDFS文件进行遍历,然后继续往下发
  6. 「接收到定时任务,再对HDFS进行遍历」这里的处理,我用的就是线程池处理

为什么选择用线程池呢?

  1. HDFS遍历其实就是IO的操作,我把这个过程给异步化,为了提高系统的吞吐量,于是我这里用的线程池。
  2. 即便遍历HDFS出现问题,我这边都有完备的监控和告警可以及时发现。

那你是怎么用线程池的呢?用Executors去创建的吗?

  1. 不是的,我这边用的ThreadPoolExecutor去创建线程池
  2. 其实看阿里巴巴开发手册就有提到,不要使用Executors去创建线程。
  3. 最主要的目的就是:使用ThreadPoolExecutor创建的线程你是更能了解线程池运行的规则,避免资源耗尽的风险
  4. ThreadPoolExecutor在构造的时候有几个重要的参数,分别是: corePoolSize (核心线程数量) 、maxim umPoolSize(最大线程数量)、keepAli veTime(线程空余时间) 、workQueue(阻塞队列)、handler(任务拒绝策略)
  5. 这几个参数应该很好理解哈,我就说下任务提交的流程,分别对应着几个参数的作用吧。
    • 首先会判断运行线程数是否小于corePoolSize,如果小于,则直接创建新的线程执行任务
    • 如果大于corePoolSize,判断workQueue阻塞队列是否已满,如果还没满,则将任务放到阻塞队列中
    • 如果workQueue阻塞队列已经满了,则判断当前线程数是否大于maximumPoolSize,如果没大于则创建新的线程执行任务
    • 如果大于maximumPoolSize,则执行任务拒绝策略(具体就是你自己实现的handler)
  6. 这里有个点需要注意下,就是workQueu e阻塞队列满了,但当前线程数小于maximumPoolSize,这时候会创建新的线程执行任务
  7. 源码就是这样实现的
  8. 不过一般我们都是将corePoolSize和maximumPoolSize设置相同数量
  9. keepAliveTime指的就是,当前运行的线程数大于核心线程数了,只要空闲时间达到了,就会对线程进行回收

那我再问一个问题,你创建线程池肯定会指定线程数的嘛,你这块是怎么考量的。

  1. 线程池指定线程数这块,首先要考量自己的业务是什么样的
  2. 是cpu密集型的还是io密集型的,假设运行应用的机器CPU核心数是N
  3. 那cpu密集型的可以先给到N+1,io密集型的可以给到2N去试试
  4. 上面这个只是一个常见的经验做法,具体究竟开多少线程,需要压测才能比较准确地定下来
  5. 线程不是说越大越好,在之前的面试我也提到过,多线程是为了充分利用CPU的资源
  6. 如果设置的线程过多,线程大量有上下文切换,这一部分也会带来系统的开销,这就得不偿失了

ThreadPoolExecutor你看过源码吗?

  1. 看过的,其实上面说的ThreadPoolExecutor几个参数,在源码的顶部注释都有
  2. 在执行的时候,重点就在于它维护了一个ctl参数,这个ctl参数的用高3位表示线程池的状态,低29位来表示线程的数量
  3. 里边用到了大量的位运算符操作,具体细节我就忘了,但是流程还是上面所讲的