一、前言
之前用线程池,都是直接用Executors.newXXXThreadPool() 粗暴的使用。在阿里巴巴开发手册中,并不建议使用Executors去创建线程池(为何不建议使用,我们后面再说),更建议用ThreadPoolExecutor去更定制化的使用线程池。所以借此机会,(又)根据数篇详细易懂的博客来进行学习和理解。
原文:https://www.cnblogs.com/javazhiyin/p/10605511.html
二、ThreadPoolExecutor 的基本属性
// 1. `ctl`,可以看做一个int类型的数字,高3位表示线程池状态,低29位表示worker数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 2. `COUNT_BITS`,`Integer.SIZE`为32,所以`COUNT_BITS`为29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 3. `CAPACITY`,线程池允许的最大线程数。1左移29位,然后减1,即为 2^29 - 1
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// 4. 线程池有5种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED
// 能接收任务且能处理阻塞队列中的任务
private static final int RUNNING = -1 << COUNT_BITS;
// 不能接收新任务,但可以处理队列中的任务。
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 不接收新任务,不处理队列任务。
private static final int STOP = 1 << COUNT_BITS;
// 所有任务都终止
private static final int TIDYING = 2 << COUNT_BITS;
// 什么都不做
private static final int TERMINATED = 3 << COUNT_BITS;
// 5. 存放任务的阻塞队列
private final BlockingQueue workQueue;
// Packing and unpacking ctl
// 6. `runStateOf()`,获取线程池状态,通过按位与操作,低29位将全部变成0
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 7. `workerCountOf()`,获取线程池worker数量,通过按位与操作,高3位将全部变成0
private static int workerCountOf(int c) { return c & CAPACITY; }
// 8. `ctlOf()`,根据线程池状态和线程池worker数量,生成ctl值
private static int ctlOf(int rs, int wc) { return rs | wc; }
/*
* Bit field accessors that don't require unpacking ctl.
* These depend on the bit layout and on workerCount being never negative.
*/
// 9. `runStateLessThan()`,线程池状态小于xx
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
// 10. `runStateAtLeast()`,线程池状态大于等于xx
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
- ctl,可以看做一个int类型的数字,高3位表示线程池状态,低29位表示线程的数量
- COUNT_BITS,Integer.SIZE为32,所以COUNT_BITS为29
- CAPACITY,线程池允许的最大线程数。1左移29位,然后减1,即为 2^29 - 1
线程池有5种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED - runStateOf(),获取线程池状态,通过按位与操作,低29位将全部变成0
- workerCountOf(),获取线程池worker数量,通过按位与操作,高3位将全部变成0
- ctlOf(),根据线程池状态和线程池worker数量,生成ctl值
- runStateLessThan(),线程池状态小于xx
- runStateAtLeast(),线程池状态大于等于xx
为了加深对这段代码的理解,我们将常量对应的二进制数以表格的形式列出来,如下所示:
常量 | 二进制数 |
---|---|
RUNNING | 11100000000000000000000000000000 |
SHUTDOWN | 00000000000000000000000000000000 |
STOP | 00100000000000000000000000000000 |
TIDYING | 01000000000000000000000000000000 |
TERMINATED | 01100000000000000000000000000000 |
CAPACITY | 00011111111111111111111111111111 |
~CAPACITY | 11100000000000000000000000000000 |
三、ThreadPoolExecutor 的构造器
我们这里只看最全的一个构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
3.1 corePoolSize
corePoolSize:当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
3.2 maxinumPollSize
maxinumPollSize:最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务(直接执行任务而不是等待)。值得注意的是如果使用了无界的任务队列这个参数就没什么效果
3.3 keepAliveTime
keepAliveTime:线程没有任务执行时的最长保持时间。当线程池中的线程数量大于corePoolSize的时候,如果此时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待一定的时间。表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用(其实就是针对核心线程外的线程),直到线程池中的线程数不大于corePoolSize(不大于的时候就不会生效,也就不会对核心线程进行销毁)。即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
3.4 runnableTaskQueue
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列
- PriorityBlockingQueue:一个具有优先级得无限阻塞队列
3.5 unit
unit:keepAliveTime的时间单位
3.6 threadFactory
threadFactory:线程工厂,用来创建线程
3.7 handler
handler:拒绝策略
- 默认策略,直接抛出异常
- 用调用者所在的线程来执行任务
- 丢弃队列中最靠前的任务,并执行当前任务
- 直接丢弃新任务
四、线程池的执行流程
4.1 线程池执行流程
- 如果运行的线程数少于corePoolSize的时候,直接创建新线程来处理任务。即使线程池中的其他任务是空闲的(会默认先把corePoolSize填充满,保证核心线程数)
- 当corePoolSize满了的时候,新任务提交时,会被推入任务队列进行等待。完成任务的核心线程会从任务队列中进行领取。
- 当任务队列被任务推满,并且继续被推新任务时,会创建非核心线程。非核心线程的数量不能超过maxinumPoolSize,被创建后立即领取推入的新任务进行执行
- 非核心线程完成新任务,处于空闲时,会从任务队列中领取任务进行执行。如果没领取到,则会一直处于空闲状态,直到keepAliveTime耗尽,该非核心线程会被销毁。
- 如果corePoolSize与maxinumPollSize相同的话,那么创建的线程池的大小是固定的,此时如果有新任务提交,而且workQueue还没满,就把请求放到workQueue中,等待有空闲的线程去workQueue中取出任务后在执行(如果corePoolSize与maxinumPollSize相同,说明不允许在线程池中创建新的线程了,让任务在队列中等待)
- 如果运行的线程数量大于maxinumPollSize,如果workQueue也已经满了,会根据指定的拒绝策略来处理该任务
判断顺序:corePoolSize -> workQueue -> maxinumPoolSize
4.2 举个栗子
直接列出这些构造参数和执行流程,不好记也不好理解。我们举一个相对形象一点的例子,来帮助我们理解:
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来,然后就将任务也分配给这4个临时工人做;
如果这14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。
五、来,跑个 Demo 给个石锤
我们这里用一个 Demo 来验证一下核心线程数、阻塞队列和最大线程数的顺序,以及拒绝策略:
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TreadPoolTestApp {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, //核心线程数
10, //最大线程数
200, //非核心线程数存活时间数
TimeUnit.MILLISECONDS, //非核心线程数存活时间单位
new LinkedBlockingQueue<>(5), //阻塞队列
new ThreadFactoryBuilder().build(), //线程工厂
new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
for (int i = 0; i < 20; i++) {
try {
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:" + executor.getPoolSize() +
",队列中等待执行的任务数目:" + executor.getQueue().size() +
",已执行完成的任务数目:" + executor.getCompletedTaskCount());
//此处等待10ms主要是为了让异常栈打印的更整齐,没有其他任何意义
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
executor.shutdown();
System.out.println("-----------------over---------------------");
System.out.println("线程池中线程数目:" + executor.getPoolSize() +
",队列中等待执行的任务数目:" + executor.getQueue().size() +
",已执行完成的任务数目:" + executor.getCompletedTaskCount());
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task-" + taskNum);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task-" + taskNum + "执行完毕");
}
}
打印结果:
正在执行task-0
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完成的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完成的任务数目:0
正在执行task-1
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行完成的任务数目:0
正在执行task-2
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行完成的任务数目:0
正在执行task-3
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行完成的任务数目:0
正在执行task-4
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行完成的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行完成的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行完成的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行完成的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行完成的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行完成的任务数目:0
正在执行task-10
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行完成的任务数目:0
正在执行task-11
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行完成的任务数目:0
正在执行task-12
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行完成的任务数目:0
正在执行task-13
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行完成的任务数目:0
正在执行task-14
java.util.concurrent.RejectedExecutionException: Task com.angryjoe.concurrency.treadpool.MyTask@6d311334 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.angryjoe.concurrency.treadpool.TreadPoolTestApp.main(TreadPoolTestApp.java:25)
java.util.concurrent.RejectedExecutionException: Task com.angryjoe.concurrency.treadpool.MyTask@214c265e rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.angryjoe.concurrency.treadpool.TreadPoolTestApp.main(TreadPoolTestApp.java:25)
java.util.concurrent.RejectedExecutionException: Task com.angryjoe.concurrency.treadpool.MyTask@7cca494b rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.angryjoe.concurrency.treadpool.TreadPoolTestApp.main(TreadPoolTestApp.java:25)
java.util.concurrent.RejectedExecutionException: Task com.angryjoe.concurrency.treadpool.MyTask@3b9a45b3 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.angryjoe.concurrency.treadpool.TreadPoolTestApp.main(TreadPoolTestApp.java:25)
java.util.concurrent.RejectedExecutionException: Task com.angryjoe.concurrency.treadpool.MyTask@58372a00 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.angryjoe.concurrency.treadpool.TreadPoolTestApp.main(TreadPoolTestApp.java:25)
-----------------over---------------------
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行完成的任务数目:0
task-0执行完毕
正在执行task-5
task-1执行完毕
正在执行task-6
task-2执行完毕
正在执行task-7
task-3执行完毕
正在执行task-8
task-4执行完毕
正在执行task-9
task-10执行完毕
task-11执行完毕
task-12执行完毕
task-13执行完毕
task-14执行完毕
task-5执行完毕
task-6执行完毕
task-7执行完毕
task-8执行完毕
task-9执行完毕
Process finished with exit code 0
- 我们设置5条核心线程,10条最大线程,阻塞队列长度为5。
- 通过 for 循环创建15 个任务,交给线程池处理
预期
- 线程池先创建 5 条核心线程去处理任务
- 然后将 5 个任务放到队列中去
- 最后创建 5 条非核心线程来执行任务
- 剩下的 5 条会触发拒绝策略,直接抛出异常
结果
可以观察打印结果中的 “线程池中线程数目”,“队列中等待执行的任务数目” 以及 “已执行完成的任务数目”。
可以看到,“线程池中线程数目” 增加到 5 就暂时不增加了,继而增加的是 “队列中等待执行的任务数目”,当 “队列中等待执行的任务数目” 也满了,则继续增加 “线程池中线程数目”。当 “线程池中线程数目” 也已经满了,就直接触发拒绝策略抛出异常。
六、结尾
本文先简单说明线程池的基本属性和执行流程。下一篇将分析线程池源码是怎样的结构,以及线程池具体是如何完成这样的调度任务的。如有理解不对的地方,还望留言,大家一起讨论,一起学习,一起进步。
参考:
https://www.jianshu.com/p/389b58856894
https://www.cnblogs.com/javazhiyin/p/10605511.html
下一节:ThreadPoolExecutor(二) —— 线程池源码分析