上一节中提到关闭线程池过程中需要对新提交的任务进行处理。这个是java.util.concurrent.RejectedExecutionHandler处理的逻辑。
在没有分析线程池原理之前先来分析下为什么有任务拒绝的情况发生。
这里先假设一个前提:线程池有一个任务队列,用于缓存所有待处理的任务,正在处理的任务将从任务队列中移除。因此在任务队列长度有限的情况下就会出现新任务的拒绝处理问题,需要有一种策略来处理应该加入任务队列却因为队列已满无法加入的情况。另外在线程池关闭的时候也需要对任务加入队列操作进行额外的协调处理。
RejectedExecutionHandler提供了四种方式来处理任务拒绝策略。
这四种策略是独立无关的,是对任务拒绝处理的四中表现形式。最简单的方式就是直接丢弃任务。但是却有两种方式,到底是该丢弃哪一个任务,比如可以丢弃当前将要加入队列的任务本身(DiscardPolicy)或者丢弃任务队列中最旧任务(DiscardOldestPolicy)。丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些额外的处理。除了丢弃任务还可以直接抛出一个异常(RejectedExecutionException),这是比较简单的方式。抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用者的处理过程。除了抛出异常以外还可以不进入线程池执行,在这种方式(CallerRunsPolicy)中任务将有调用者线程去执行。
上面是一些理论知识,下面结合一些例子进行分析讨论。
package xylz.study.concurrency;
import java.lang.reflect.Field;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy;
public class ExecutorServiceDemo {
static void log(String msg) {
System.out.println(System.currentTimeMillis() + " -> " + msg);
}
static int getThreadPoolRunState(ThreadPoolExecutor pool) throws Exception {
Field f = ThreadPoolExecutor.class.getDeclaredField("runState");
f.setAccessible(true);
int v = f.getInt(pool);
return v;
}
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 10; i++) {
final int index = i;
pool.submit(new Runnable() {
public void run() {
log("run task:" + index + " -> " + Thread.currentThread().getName());
try {
Thread.sleep(1000L);
} catch (Exception e) {
e.printStackTrace();
}
log("run over:" + index + " -> " + Thread.currentThread().getName());
}
});
}
log("before sleep");
Thread.sleep(4000L);
log("before shutdown()");
pool.shutdown();
log("after shutdown(),pool.isTerminated=" + pool.isTerminated());
pool.awaitTermination(1000L, TimeUnit.SECONDS);
log("now,pool.isTerminated=" + pool.isTerminated() + ", state="
+ getThreadPoolRunState(pool));
}
}
第一种方式直接丢弃(DiscardPolicy)的输出结果是:
1294494050696 -> run task:0
1294494050696 -> before sleep
1294494051697 -> run over:0 -> pool-1-thread-1
1294494051697 -> run task:1
1294494052697 -> run over:1 -> pool-1-thread-1
1294494054697 -> before shutdown()
1294494054697 -> after shutdown(),pool.isTerminated=false
1294494054698 -> now,pool.isTerminated=true, state=3
对于上面的结果需要补充几点。
- 线程池设定线程大小为1,因此输出的线程就只有一个”pool-1-thread-1”,至于为什么是这个名称,以后会分析。
- 任务队列的大小为1,因此可以输出一个任务执行结果。但是由于线程本身可以带有一个任务,因此实际上一共执行了两个任务(task0和task1)。
- shutdown()一个线程并不能理解是线程运行状态位terminated,可能需要稍微等待一点时间。尽管这里等待时间参数是1000秒,但是实际上从输出时间来看仅仅等了约1ms。
- 直接丢弃任务是丢弃将要进入线程池本身的任务,所以当运行task0是,task1进入任务队列,task2~task9都被直接丢弃了,没有运行。
如果把策略换成丢弃最旧任务(DiscardOldestPolicy),结果会稍有不同。
1294494484622 -> run task:0
1294494484622 -> before sleep
1294494485622 -> run over:0 -> pool-1-thread-1
1294494485622 -> run task:9
1294494486622 -> run over:9 -> pool-1-thread-1
1294494488622 -> before shutdown()
1294494488622 -> after shutdown(),pool.isTerminated=false
1294494488623 -> now,pool.isTerminated=true, state=3
这里依然只是执行两个任务,但是换成了任务task0和task9。实际上task1~task8还是进入了任务队列,只不过被task9挤出去了。
对于异常策略(AbortPolicy)就比较简单,这回调用线程的任务执行。
对于调用线程执行方式(CallerRunsPolicy),输出的结果就有意思了。
1294496076266 -> run task:2 -> main
1294496076266 -> run task:0 -> pool-1-thread-1
1294496077266 -> run over:0 -> pool-1-thread-1
1294496077266 -> run task:1 -> pool-1-thread-1
1294496077266 -> run over:2 -> main
1294496077266 -> run task:4 -> main
1294496078267 -> run over:4 -> main
1294496078267 -> run task:5 -> main
1294496078267 -> run over:1 -> pool-1-thread-1
1294496078267 -> run task:3 -> pool-1-thread-1
1294496079267 -> run over:3 -> pool-1-thread-1
1294496079267 -> run over:5 -> main
1294496079267 -> run task:7 -> main
1294496079267 -> run task:6 -> pool-1-thread-1
1294496080267 -> run over:7 -> main
1294496080267 -> run task:9 -> main
1294496080267 -> run over:6 -> pool-1-thread-1
1294496080267 -> run task:8 -> pool-1-thread-1
1294496081268 -> run over:9 -> main
1294496081268 -> before sleep
1294496081268 -> run over:8 -> pool-1-thread-1
1294496085268 -> before shutdown()
1294496085268 -> after shutdown(),pool.isTerminated=false
1294496085269 -> now,pool.isTerminated=true, state=3
由于启动线程有稍微的延时,因此一种可能的执行顺序是这样的。
- 首先pool-1-thread-1线程执行task0,同时将task1加入任务队列(submit(task1))。
- 对于task2,由于任务队列已经满了,因此有调用线程main执行(execute(task2))。
- 在mian等待task2任务执行完毕,对于任务task3,由于此时任务队列已经空了,因此task3将进入任务队列。
- 此时main线程是空闲的,因此对于task4将由main线程执行。此时pool-1-thread-1线程可能在执行任务task1。任务队列中依然有任务task3。
- 因此main线程执行完毕task4后就立即执行task5。
- 很显然task1执行完毕,task3被线程池执行,因此task6进入任务队列。此时task7被main线程执行。
- task6开始执行时,task8进入任务队列。main线程开始执行task9。
- 然后线程池执行线程task8结束。
- 整个任务队列执行完毕,线程池完毕。
如果有兴趣可以看看ThreadPoolExecutor中四种RejectedExecutionHandler的源码,都非常简单。