近期在ETL项目中,增加一个用于监控队列数和当前线程数之间的关系,并动态调节线程池大小的一个功能。其作用机制即是指当发现队列中任务太多时,能够增大线程数,以达到使用更多的线程来运行任务的目的。相应的伪代码如下所示:

extThreadPoolExecutor.setCorePoolSize(newCorePoolSize);
extThreadPoolExecutor.prestartCoreThread();

但是在后面通过监控发现以下的信息

队列总数:1670,核心线程数:500,活跃线程数:200

即相应的核心线程数是已经作了相应的调整,但是活跃线程数,却始终没有上升。没有达到调节线程数的目的。

从理论上说,如果增大了核心线程数,那么线程数会在新任务时,会自动创建新的线程来进行相应的任务,并且我们通过手动启动核心线程来强制运行任务,因此也不会出现线程并没有创建的问题(详细可以查看prestartCoreThread的作用)。

重新查看相应的源代码,发现此线程池在设计层面是这样进行定义的:

corePoolSize=maximumPoolSize

即在这次调整中,并没有调整最大线程池大小,即导致core >max的现象发生。官方文档中并没有说这种情况下会出现什么情况,因此只有实际去查看源码。跟踪maximusPoolSize在类ThreadPoolExecutor中的使用,最终可以看到在方法getTask在如下使用,代码如下所示:

private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
    int c = ctl.get();
    int rs = runStateOf(c);
//......
    int wc = workerCountOf(c);
    // Are workers subject to culling?
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    //这里如果当前线程数工作线程大于max,则直接返回null
    if ((wc > maximumPoolSize || (timed && timedOut))
        && (wc > 1 || workQueue.isEmpty())) {
        if (compareAndDecrementWorkerCount(c))
            return null;
        continue;
    }
    //......
}

在这里因为,当前工作线程数肯定大于max(因为使用了prestartCoreThread),会返回null,而此调用的调用者为 runWorker,即实际上为刚刚创建好的线程准备去拿任务,并没有拿到,因此会正常地退出(因为,在线程池中设置了allowCoreThreadTimeOut).整个流程如下所示:

  1. prestartCoreThread工作线程数+1
  2. 运行刚新建的线程worker
  3. worker获取task
  4. 没有拿到task
  5. worker正常结束
  6. 清理worker,工作线程数-1

最终的效果就是刚启动的线程马上就结束了,导致工作线程始终不能增加,即不能创建新的线程。

找到了上面的问题,解决方法也很简单,即同时调节相应的maximusPoolSize,即可。在调节代码中按照以下的步骤进行处理即可

extThreadPoolExecutor.setMaximusPoolSize(newCorePoolSize);
extThreadPoolExecutor.setCorePoolSize(newCorePoolSize);

上面代码,先设置max的目的在于保证新创建的核心线程能够获取到任务或者在获取任务的步骤当中,而不是直接退出.

附:在当前的线程池当中,我们为什么将corePoolSize设置成maximum一样的原因也在于需要手动控制线程数的大小,同时设置allowCoreTimeOut以保证即使core设置成很大也没有关系,在空闲时core线程也会终结。也可以理解为与core设置为0,max设置为相应值一样的效果。因为,在应用中并不是通过executor.addTask来添加任务(而是通过直接操纵queue来添加任务),因此在应用中在queue.add(task)时,通过prestartCoreThread来手动控制新线程的创建过程。