首页 > 代码库 > 理解ThreadPoolExecutor源码(二)execute函数的巧妙设计和阅读心得
理解ThreadPoolExecutor源码(二)execute函数的巧妙设计和阅读心得
ThreadPoolExecutor.execute()源码提供了大量注释来解释该方法的设计考虑。下面的源码来自jdk1.6.0_37
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated } }
使用这么多if-else就是为了性能考虑,减小锁的使用范围,避免execute方法整个执行过程中都持有mainLock锁。可以看到只有调用addIfUnderCorePoolSize、ensureQueuedTaskHandled、addIfUnderMaximumPoolSize这3个方法才需要持有锁。如果新提交的任务,不会进入这3个方法,那么就不需要持有锁。我们来看下,execute方法的设计是否能够有效地减少进入这3个方法的次数,实现快进快出。
第4行代码poolSize >= corePoolSize,为什么要加这个判断呢?
如果通过prestartAllCoreThreads事先启动了所有核心线程,或者是提交的任务已经让当前大小poolSize达到了核心大小 corePoolSize,并且核心线程不会死亡(allowCoreThreadTimeOut=false不允许核心线程超时退出,并且任务执行过程没有抛出异常导致线程退出),那么线程池中的线程数一定不会比corePoolSize小,此后如果再提交新任务,那么就不会进入addIfUnderCorePoolSize方法。poolSize >= corePoolSize就是为了减少进入addIfUnderCorePoolSize函数的次数,减少锁的获取和释放次数。如果poolSize<corePoolSize,那么就会进入addIfUnderCorePoolSize,该方法会在加锁情况下,判断线程池的状态和当前大小,然后决定是否需要新加线程来处理任务。由于addIfUnderCorePoolSize会持有mainLock锁,所以可以防止其他线程对线程池的并发修改。也就说可以保证:在不需要添加新线程的时候,就不会添加。The call to addIfUnderCorePoolSize rechecks runState and pool size under lock (they change only under lock) so prevents false alarms that would add threads when it shouldn‘t。源码中这段注释,说的就是这个效果。
第5行代码if (runState == RUNNING && workQueue.offer(command)),如果程序能走到这行代码,从任务提交者的角度来看,此时线程池大小已经达到了核心大小(虽然事实情况不一定如此,因为没有加锁,存在并发修改的可能;而且线程池中的线程也可能会死亡。如果满足条件:
runState == RUNNING && workQueue.offer(command) 这意味着:线程池仍然处于运行状态,并且任务排队已经成功。为什么程序执行到这里依然不能结束,还是需要走之后的代码呢?因为没有加锁,判断条件不一定可靠。考虑下面2个场景:如果任务刚开始调用offer(还没有成功地插入到阻塞队列中),线程池中的线程全部死亡,那么就没有线程来处理当前提交的这个任务了;如果任务刚刚排队成功,别的线程调用了shutdownNow()关闭了线程池,那么按照shutdownNow函数的语义,这个任务不应该被处理。由于存在这2种特殊情况,所以必须进行后续的处理。but may also fail to add them when they should. This is compensated within the following steps.这就是说:addIfUnderCorePoolSize能够保证不需要新线程的时候就不添加,但是不能保证需要添加新线程的时候就添加。所以让任务排队成功的时候,需要在锁的保护下,判断是否需要删除这个任务,或者是否需要新增线程来处理任务。
第6行代码if (runState != RUNNING || poolSize == 0),如果任务成功排队(workQueue.offer()返回true)后,线程池被关闭或者没有存活的线程,那么就需要执行ensureQueuedTaskHandled(command),也就是说这种情况下,任务可能没有得到合适的处置。如果任务成功排队后,线程池仍然处在运行状态,而且有存活的线程,那么就能够确保该新提交的任务一定会被处理。为什么会这样呢?我们知道如果想关闭ThreadPoolExecutor,只有3种途径:调用shutdown方法,调用shutdownNow方法,线程池最后一个工作者退出(对应workerDone方法)。查看源码我们知道,这3个可能导致线程池关闭的方法,最终都会调用tryTerminate()方法。也就是说如果线程池想要终止,就必须通过该方法。
/** * Transitions to TERMINATED state if either (SHUTDOWN and pool * and queue empty) or (STOP and pool empty), otherwise unless * stopped, ensuring that there is at least one live thread to * handle queued tasks. * * This method is called from the three places in which * termination can occur: in workerDone on exit of the last thread * after pool has been shut down, or directly within calls to * shutdown or shutdownNow, if there are no live threads. */ private void tryTerminate() { if (poolSize == 0) { int state = runState; if (state < STOP && !workQueue.isEmpty()) { state = RUNNING; // disable termination check below Thread t = addThread(null); if (t != null) t.start(); } if (state == STOP || state == SHUTDOWN) { runState = TERMINATED; termination.signalAll(); terminated(); } } }
当线程池的线程池数是0的时候,如果线程池是running或者shutdown状态,并且任务队列不为空,那么就会新增1个线程来处理任务;如果线程池是状态,或者处于shutdown状态并且任务队列为空,那么线程池就会退出。也就是说,如果任务排队成功后,线程池还没有终止,那么该任务一定会得到合理的处置。
如果在任务插入之前或者插入的过程中,线程池不在运行状态runState != RUNNING 或者没有存活的线程poolSize == 0,那么就需要自己考虑下:如何处理该提交的任务,这通过ensureQueuedTaskHandled来完成。如果if (runState == RUNNING && workQueue.offer(command))条件是真,那么随后if (runState != RUNNING || poolSize == 0)基本上很少出现,大部分场景下都不需要执行ensureQueuedTaskHandled方法,就不需要获取和释放锁。下面我们通过源码看下,ensureQueuedTaskHandled方法是如何处理异常场景的。
/** * Rechecks state after queuing a task. Called from execute when * pool state has been observed to change after queuing a task. If * the task was queued concurrently with a call to shutdownNow, * and is still present in the queue, this task must be removed * and rejected to preserve shutdownNow guarantees. Otherwise, * this method ensures (unless addThread fails) that there is at * least one live thread to handle this task * @param command the task */ private void ensureQueuedTaskHandled(Runnable command) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); boolean reject = false; Thread t = null; try { int state = runState; if (state != RUNNING && workQueue.remove(command)) reject = true; else if (state < STOP && poolSize < Math.max(corePoolSize, 1) && !workQueue.isEmpty()) t = addThread(null); } finally { mainLock.unlock(); } if (reject) reject(command); else if (t != null) t.start(); }
该方法持有mainLock锁,所以可以防止线程池的并发修改。如果线程池不在running状态(state != RUNNING ),并且新提交的任务还驻留在任务队列中(workQueue.remove(command)返回true),那么当前提交的任务会被拒绝执行(调用reject(comma)方法)。也就是说,只要线程池不是running状态,那么就一定会拒绝执行当前提交的任务,除非该任务已经被线程池中的线程处理了(workQueue.remove返回false)。这里不区分到底是shutdown()关闭的线程池,还是通过shutdownNow关闭的线程池。如果新提交一个任务,并且执行流程进入了ensureQueuedTaskHandled()函数,那么该任务可能会被拒绝执行,也可能会被正常执行。如果线程池是running或者shutdown状态(state < STOP),并且线程池中已经没有存活的线程,并且任务队列非空,那么就需要新加1个线程,来处理等待执行的任务。
可以看到:通过多次无锁的条件判断,能够有效地减少提交任务时对mainLock锁的竞争;也能够确在并发执行的情况下,当前新提交的任务和线程池中等待执行的任务,都能得到合适的处理。不知道大师Doug Lea是如何想到这么巧妙的设计和实现execute()方法的!虽然execute方法能够提高性能,但是牺牲了代码的可读性和简易性。对于并发程序,还是那句经典的老话make it right before you make it faster。
理解ThreadPoolExecutor源码(二)execute函数的巧妙设计和阅读心得