(手机横屏看源码更方便)
注:java源码分析部分如无特殊说明均基于 java8 版本。
注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类。
简介前面我们一起学习了Java中线程池的体系结构、构造方法和生命周期,本章我们一起来学习线程池中普通任务到底是怎么执行的。
建议学习本章前先去看看彤哥之前写的《死磕 java线程系列之自己动手写一个线程池》那两章,有助于理解本章的内容,且那边的代码比较短小,学起来相对容易一些。
问题(2)任务又是在哪里被执行的?
(3)线程池中有哪些主要的方法?
(4)如何使用Debug模式一步一步调试线程池?
使用案例我们创建一个线程池,它的核心数量为5,最大数量为10,空闲时间为1秒,队列长度为5,拒绝策略打印一句话。
如果使用它运行20个任务,会是什么结果呢?
public class ThreadPoolTest01 { public static void main(String[] args) { // 新建一个线程池 // 核心数量为5,最大数量为10,空闲时间为1秒,队列长度为5,拒绝策略打印一句话 ExecutorService threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println(currentThreadName() + ", discard task"); } }); // 提交20个任务,注意观察num for (int i = 0; i < 20; i++) { int num = i; threadPool.execute(()->{ try { System.out.println(currentThreadName() + ", "+ num + " running, " + System.currentTimeMillis()); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }); } } private static String currentThreadName() { return Thread.currentThread().getName(); } }构造方法的7个参数我们就不详细解释了,有兴趣的可以看看《死磕 java线程系列之线程池深入解析——构造方法》那章。
我们一起来看看一次运行的结果:
pool-1-thread-1, 0 running, 1572678434411 pool-1-thread-3, 2 running, 1572678434411 pool-1-thread-2, 1 running, 1572678434411 pool-1-thread-4, 3 running, 1572678434411 pool-1-thread-5, 4 running, 1572678434411 pool-1-thread-6, 10 running, 1572678434412 pool-1-thread-7, 11 running, 1572678434412 pool-1-thread-8, 12 running, 1572678434412 main, discard task main, discard task main, discard task main, discard task main, discard task // 【本文由公从号“彤哥读源码”原创】 pool-1-thread-9, 13 running, 1572678434412 pool-1-thread-10, 14 running, 1572678434412 pool-1-thread-3, 5 running, 1572678436411 pool-1-thread-1, 6 running, 1572678436411 pool-1-thread-6, 7 running, 1572678436412 pool-1-thread-2, 8 running, 1572678436412 pool-1-thread-7, 9 running, 1572678436412注意,观察num值的打印信息,先是打印了0~4,再打印了10~14,最后打印了5~9,竟然不是按顺序打印的,为什么呢?
让我们一步一步debug进去查看。
execute()方法execute()方法是线程池提交任务的方法之一,也是最核心的方法。
// 提交任务,任务并非立即执行,所以翻译成执行任务似乎不太合适 public void execute(Runnable command) { // 任务不能为空 if (command == null) throw new NullPointerException(); // 控制变量(高3位存储状态,低29位存储工作线程的数量) int c = ctl.get(); // 1. 如果工作线程数量小于核心数量 if (workerCountOf(c) < corePoolSize) { // 就添加一个工作线程(核心) if (addWorker(command, true)) return; // 重新获取下控制变量 c = ctl.get(); } // 2. 如果达到了核心数量且线程池是运行状态,任务入队列 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); // 再次检查线程池状态,如果不是运行状态,就移除任务并执行拒绝策略 if (! isRunning(recheck) && remove(command)) reject(command); // 容错检查工作线程数量是否为0,如果为0就创建一个 else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 3. 任务入队列失败,尝试创建非核心工作线程 else if (!addWorker(command, false)) // 非核心工作线程创建失败,执行拒绝策略 reject(command); }关于线程池状态的内容,我们这里不拿出来细讲了,有兴趣的可以看看《死磕 java线程系列之线程池深入解析——生命周期》那章。
提交任务的过程大致如下:
(1)工作线程数量小于核心数量,创建核心线程;
(2)达到核心数量,进入任务队列;
(3)任务队列满了,创建非核心线程;
(4)达到最大数量,执行拒绝策略;
其实,就是三道坎——核心数量、任务队列、最大数量,这样就比较好记了。
流程图大致如下: