昨天正好面试的时候,问了面试者线程池的问题,问题时这样的:”ThreadPoolExecutor中,假如corePoolSize=5, maximumPoolSize=10, workQueue的长度也是5,此时我向这个线程池中提交6个Runnable,线程池此时如何处理这第六个Runnable?”。
这道题目很多面试者容易犯错,因为直观上,我们感觉线程池应该这样处理:
(1) 当线程池中的线程数目小于maximumPoolSize时,新来一个任务的话,应该新创建一个线程来处理;
(2) 当线程池中存活的线程的数目达到了maximumPoolSize之后,新来的任务应该会被防到队列中,等在线程忙完之后再来处理。
实际情况是这样吗?先做一个实验:
public class ThreadPoolTest {
public static void main(String[] args) throws Exception{
ExecutorService executorService = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5));
List<Future> list = new ArrayList<Future>();
//创建5个长任务,每个长任务会运行1分钟左右的时候
int i = 5;
while (i-- > 0) {
list.add(executorService.submit(new LongRunnable()));
}
//提交一个短任务,段任务会打印它的运行时间
list.add(executorService.submit(new ShortRunnable()));
//打印当前提交时间
System.out.println("submit time:" + new Date());
//等待所有的任务执行完
for(Future future : list) {
future.get();
}
executorService.shutdown();
}
private static class ShortRunnable implements Runnable {
@Override
public void run() {
System.out.println("run time: " + new Date());
}
}
private static class LongRunnable implements Runnable {
@Override
public void run() {
int i = 60;
while (i-- > 0) {
try {
Thread.sleep(1000L);
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
结果:
submit time:Tue Feb 23 15:05:28 CST 2016
run time: Tue Feb 23 15:06:28 CST 2016
从结果中可以看出,第六个提交的任务的运行时间比他的提交时间大概晚了1分钟左右,也就是说,当我们提交第六个任务的时候,线程池并没有创建一个新的线程来运行它,而是等最开始创建的5个线程中有线程空闲了才来运行第六个任务。这时候我们提交的这个任务线程池把它放在哪的呢?应该是放在队列中的。
下面我们修改测试代码, 把list.add(executorService.submit(new ShortRunnable()));
修改成
i = 6;
while (i-- > 0) {
list.add(executorService.submit(new ShortRunnable()));
}
再运行下看看:
run time: Tue Feb 23 15:20:06 CST 2016
submit time:Tue Feb 23 15:20:06 CST 2016
run time: Tue Feb 23 15:20:06 CST 2016
run time: Tue Feb 23 15:20:06 CST 2016
run time: Tue Feb 23 15:20:06 CST 2016
run time: Tue Feb 23 15:20:06 CST 2016
run time: Tue Feb 23 15:20:06 CST 2016
此时我们发现, 提交的6个任务全部瞬间运行完了。这也说明了线程池的运行过程是这样的:
(1) 当提交一个任务交给线程池,而此时线程池中并没有空闲的线程来处理时, 线程池会把任务放在队列中缓存起来;
(2) 如果缓存队列满了,新提交一个任务时,此时线程池会判断池中的线程数目是否大于或等于maximumPoolSize, 如果没有大于或等于,则新创建一个线程来处理改任务。如果线程池中的线程数目已经大于或等于maximumPoolSize, 则拒绝服务。
我们看ThreadPoolExecutor的源代码:
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
}
}
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
从代码中可以看出,
(1) 当线程池中添加任务时,线程池首先会检查当前存活的线程是否已经达到了corePoolSize;
(2) 如果未达到,则创建一个新的线程来处理该任务;
(3) 如果存活线程数目已达到corePoolSize,或者创建新的执行线程失败,则执行runState == RUNNING && workQueue.offer(command)
把任务添加到队列中。
(4) 我们知道阻塞队列的offer方法是不阻塞的, 也就是当队列满了之后,workQueue.offer(command)会返回false,此时, 则添加线程来执行任务addIfUnderMaximumPoolSize(command)
;
(5) 如果addIfUnderMaximumPoolSize(command)
返回false, 则说明线程池中存活的线程数目已经达到了最大的线程数目, 则拒绝执行。