inside.java.concurrency 31.thread pool.part4_rejectedpolicy
DESCRIPTION
深入浅出 Java Concurrency (31): 线程池 part 4 线程池 任务拒绝策略TRANSCRIPT
深入浅出 Java Concurrency (31): 线程池 part 4 Executor
任务拒绝策略
上一节中提到关闭线程池过程中需要对新提交的任务进行处理。这个是
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. 线程池设定线程大小为 1,因此输出的线程就只有一个”pool-1-thread-1”,至于为什
么是这个名称,以后会分析。
2. 任务队列的大小为 1,因此可以输出一个任务执行结果。但是由于线程本身可以带
有一个任务,因此实际上一共执行了两个任务(task0 和 task1)。
3. shutdown()一个线程并不能理解是线程运行状态位 terminated,可能需要稍微等待
一点时间。尽管这里等待时间参数是 1000 秒,但是实际上从输出时间来看仅仅等
了约 1ms。
4. 直接丢弃任务是丢弃将要进入线程池本身的任务,所以当运行 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
由于启动线程有稍微的延时,因此一种可能的执行顺序是这样的。
1. 首先 pool-1-thread-1 线程执行 task0,同时将 task1 加入任务队列(submit(task1))。
2. 对于 task2,由于任务队列已经满了,因此有调用线程 main 执行(execute(task2))。
3. 在 mian 等待 task2 任务执行完毕,对于任务 task3,由于此时任务队列已经空了,
因此 task3 将进入任务队列。
4. 此时 main 线程是空闲的,因此对于 task4 将由 main 线程执行。此时 pool-1-thread-1
线程可能在执行任务 task1。任务队列中依然有任务 task3。
5. 因此 main 线程执行完毕 task4 后就立即执行 task5。
6. 很显然 task1 执行完毕,task3 被线程池执行,因此 task6 进入任务队列。此时 task7
被 main 线程执行。
7. task6 开始执行时,task8 进入任务队列。main 线程开始执行 task9。
8. 然后线程池执行线程 task8 结束。
9. 整个任务队列执行完毕,线程池完毕。
如果有兴趣可以看看 ThreadPoolExecutor 中四种 RejectedExecutionHandler 的源码,都非
常简单。
上一篇:线程池 part 3 Executor 生命周期 目 录 下一篇: 线程池 part5 可调度的
线程池 ScheduledExecutorService