Java并发与多线程-详解线程池
什么是线程池?
在此,我们参考一下百科的定义:
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。
线程池的作用?
了解线程池作用之前,我们先来看一下,CPU的上下文切换
,有多费时间:
拿一台主频2.6G的电脑来说,每秒可以执行
2.6*10^9
个指令,每个指令只需要0.38ns
,为了方便理解,我们把这个时间单位,换算成人类世界的1秒
。
一次 CPU 上下文切换(系统调用)需要大约1500ns
,也就是1.5us
(这个数字采用的是单核 CPU 线程平均时间),换算成人类时间大约是65分钟
,嗯,也就是一个小时
。我们也知道上下文切换是很耗时的行为,毕竟每次浪费一个小时,也很让人有罪恶感的。上下文切换更恐怖的事情在于,这段时间里 CPU没有做任何有用的计算,只是切换了两个不同进程的寄存器和内存状态;而且这个过程还破坏了缓存,让后续的计算更加耗时。
备注:上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程或线程切换到另一个进程或线程。
如果想让程序运行的更快,我们需要减少CPU上下文切换的次数。
所以,需要避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。使用线程池,可以有效避免类似的问题。线程池解决的核心问题就是资源管理问题。对使用到的线程进行统一管理,避免创建不必要的线程。
使用线程池,还可以带来如下好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
既然线程池有这么多好处,那我们如何使用呢?
首先,要用好线程池,需要知道线程池的生命周期以及线程池的主要参数配置。
线程池生命周期
ThreadPoolExecutor的运行状态有5种:
- RUNNING
- SHUTDOWN
- STOP
- TIDYING
- TERMINATED
源码定义如下:
java.util.concurrent.ThreadPoolExecutor.ctl
文档注释摘录
The runState provides the main lifecycle control, taking on values:
RUNNING: Accept new tasks and process queued tasks
(译:接受新任务并处理排队的任务)
SHUTDOWN: Don't accept new tasks, but process queued tasks
(译:不接受新任务,但处理已排队的任务)
STOP: Don't accept new tasks, don't process queued tasks,
and interrupt in-progress tasks
(译:不接受新任务,不处理排队的任务,并中断正在进行的任务)
TIDYING: All tasks have terminated, workerCount is zero,
the thread transitioning to state TIDYING
will run the terminated() hook method
(译:所有任务已终止,workerCount为零,线程转换到 TIDYING 状态,会运行terminated()钩子方法)
TERMINATED: terminated() has completed
(译:terminated()方法执行完毕)
图:线程池5种状态-流程图
状态 | 是否接受新任务 | 排队任务的处理 | 其他 |
---|---|---|---|
RUNNING | 接受新任务 | 处理排队的任务 | 正常运行 |
SHUTDOWN | 不接受新任务 | 处理排队的任务 | 不接受新任务,但处理已排队的任务 |
STOP | 不接受新任务 | 不处理排队的任务 | 不接受新任务,不处理排队的任务,并中断正在进行的任务 |
TIDYING | 不接受新任务 | - | 所有任务已终止 |
TERMINATED | 不接受新任务 | - | terminated()方法执行完毕 |
详解线程池参数
图:线程池参数列表.png
- 第一个参数设置核心线程数。默认情况下核心线程会一直存活。
- 第二个参数设置最大线程数。决定线程池最多可以创建的多少线程。
- 第三个参数和第四个参数用来设置线程空闲时间,和空闲时间的单位,当线程闲置超过空闲时间就会被销毁。可以通过 allowCoreThreadTimeOut 方法来允许核心线程被回收。
- 第五个参数设置缓冲队列,上图中左下方的三个队列是设置线程池时常使用的缓冲队列。其中
ArrayBlockingQueue
是一个有界队列,就是指队列有最大容量限制。LinkedBlockingQueue
是无界队列,就是队列不限制容量。最后一个是SynchronousQueue
,是一个同步队列,内部没有缓冲区。 - 第六个参数设置线程池工厂方法,线程工厂用来创建新线程,可以用来对线程的一些属性进行定制,例如线程的 group、线程名、优先级等。一般使用默认工厂类即可。
- 第七个参数设置线程池满时的拒绝策略。如上图右下方所示有四种策略,
Abort
策略在线程池满后,提交新任务时会抛出RejectedExecutionException
,这个也是默认的拒绝策略。Discard 策略会在提交失败时对任务直接进行丢弃。CallerRuns 策略会
在提交失败时,由提交任务的线程直接执行提交的任务。DiscardOldest
策略会丢弃最早提交的任务。
接着,我们需要了解一下线程池的执行流程:
线程池执行流程
图:线程池任务执行流程.png
- 向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。
- 如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。
- 如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就创建新线程来执行任务。
- 如果已经达到了最大线程数,则执行指定的拒绝策略。
这里需要注意队列的判断与最大线程数判断的顺序,不要搞反。
了解了理论,接下来,我们通过一个实战,来实地观察一下线程池任务的执行流程。
实战源码
- 程序目的:观察
ThreadPoolExecutor
的执行流程
包含如下组成部分: - 一个监听线程池状态的类:
MyMonitorThread
,每1秒输出一次线程池状态。 - 一个
RejectedExecutionHandlerImpl
,用来执行拒绝策略,会打印那些任务被拒绝了。 - 一个工作线程定义:
WorkerThread
会模拟2秒的任务执行耗时。 - 主程序:
WorkerPoolApplication
用来执行程序
MyMonitorThread.java
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
/**
* <pre>
* 监控线程池状态(每隔N秒,输出一次线程池的状态)
* 分别会打印如下信息:
* </pre>
* created at 2019-05-29 14:12
* @author lerry
*/
@Slf4j
public class MyMonitorThread implements Runnable {
/**
* 持有被监控的线程池对象
*/
private ThreadPoolExecutor threadPoolExecutor;
/**
* 每隔多久执行一次
*/
private int delay;
/**
* 如果为false、则关闭监听
*/
private boolean isRun = true;
public MyMonitorThread(ThreadPoolExecutor threadPoolExecutor, int delay) {
this.threadPoolExecutor = threadPoolExecutor;
this.delay = delay;
}
public void shutDown() {
this.isRun = false;
}
@Override
public void run() {
while (isRun) {
// 获取等待队列
BlockingQueue<Runnable> queue = this.threadPoolExecutor.getQueue();
// 等待队列转为仅存储 指令名称 的List
List<String> queueList = queue.stream().map(r -> {
if (r instanceof WorkerThread) {
return ((WorkerThread) r).getCommand();
}
else {
return "";
}
}).collect(Collectors.toList());
// 日志记录线程池状态
/*
* poolSize:池中的当前线程数
* corePoolSize: 核心线程数
* Active:当前主动执行任务的近似线程数量
* Completed:已完成执行的任务的总数(由于任务和线程的状态在计算过程中可能会动态变化,因此返回的值仅是一个近似值,而在连续的调用中不会降低。)
* TaskCount:计划执行的任务数
* queue:缓冲队列
* isShutdown:如果此执行程序已关闭,则返回true
* isTerminated:观察线程池是否终结
*/
log.info("poolSize/corePoolSize [{}/{}] Active: {}, Completed: {}, Task: {}, queue:{},isShutdown: {}, isTerminated: {}",
this.threadPoolExecutor.getPoolSize(),
this.threadPoolExecutor.getCorePoolSize(),
this.threadPoolExecutor.getActiveCount(),
this.threadPoolExecutor.getCompletedTaskCount(),
this.threadPoolExecutor.getTaskCount(),
queueList,
this.threadPoolExecutor.isShutdown(),
this.threadPoolExecutor.isTerminated());
// 间隔N秒输出一次线程池状态
try {
Thread.sleep(delay * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
RejectedExecutionHandlerImpl.java
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.extern.slf4j.Slf4j;
/**
* 还可以创建自己的 RejectedExecutionHandler 实现来处理没有放在工作队列里的任务。
* rejected:拒绝的
* created at 2019-05-29 14:09
* @author lerry
*/
@Slf4j
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
WorkerThread workerThread = null;
if (r instanceof WorkerThread) {
workerThread = (WorkerThread) r;
}
log.info("[{}] is rejected(被拒绝/驳回)", workerThread.getCommand());
}
}
WorkerThread.java
/**
* 工作线程
* created at 2019-05-29 11:25
* @author lerry
*/
@Slf4j
public class WorkerThread implements Runnable {
/**
* 执行的任务编号
*/
private String command;
/**
* 模拟工作线程的执行耗时(单位:秒)
*/
private int executeDuration;
public WorkerThread(String command) {
this.command = command;
}
public WorkerThread(String command, int executeDuration) {
this.command = command;
this.executeDuration = executeDuration;
}
@Override
public void run() {
log.info("{} Start. Command = {}", Thread.currentThread().getName(), command);
processCommand();
log.info("{} End. Command = {}", Thread.currentThread().getName(), command);
}
/**
* 模拟任务执行耗时
*/
private void processCommand() {
try {
Thread.sleep(executeDuration * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getCommand() {
return command;
}
}
WorkerPoolApplication.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* <pre>
* 程序目的:观察ThreadPoolExecutor的执行流程
* 包含:一个监听线程池状态的类:MyMonitorThread,每1秒输出一次线程池状态。
* 一个RejectedExecutionHandlerImpl,用来执行拒绝策略,会打印那些任务被拒绝了。
* 一个工作线程定义:WorkerThread 会模拟2秒的执行时间。
* 主程序:WorkerPoolApplication 用来执行程序
* 请注意:在初始化 ThreadPoolExecutor 时,核心线程数大小设为2、最大线程数设为5、缓冲队列大小设为2。
* 所以,一共提交10个任务的情况下,前2个任务,线程池会创建核心线程、并执行该任务;
* 第3——4个任务,会进入等待队列,队列满了之后,
* 第5——7个任务,会继续创建线程、执行该任务;
* 最后的第8、9、10个任务,会执行拒绝策略,交由RejectedExecutionHandlerImpl 处理,被rejected。
* </pre>
* created at 2019-05-29 14:16
* @author lerry
*/
@Slf4j
public class WorkerPoolApplication {
public static void main(String[] args) throws InterruptedException {
RejectedExecutionHandlerImpl rejectedExecutionHandler = new RejectedExecutionHandlerImpl();
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 线程池
/*
* corePoolSize:核心线程数 默认情况下核心线程会一直存活
* maximumPoolSize:最大线程数 决定线程池最多可以创建的多少线程
* keepAliveTime、unit:线程空闲时间,和空闲时间的单位 当线程闲置超过空闲时间就会被销毁
* workQueue:缓冲队列
* threadFactory:设置线程池工厂方法,线程工厂用来创建新线程,可以用来对线程的一些属性进行定制,例如线程的 group、线程名、优先级等
* RejectedExecutionHandler: 设置线程池满时的拒绝策略
*/
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
threadFactory,
rejectedExecutionHandler
);
// 启动监控线程
MyMonitorThread monitor = new MyMonitorThread(executorPool, 1);
new Thread(monitor).start();
for (int i = 1; i <= 10; i++) {
executorPool.execute(new WorkerThread("cmd" + i, 2));
}
log.info("关闭线程池");
executorPool.shutdown();
for (; ; ) {
// 线程池终止后,关闭监控线程
if (executorPool.isTerminated()) {
// 等待监视器打印线程池 isTerminated: true 的状态
Thread.sleep(1_000);
monitor.shutDown();
break;
}
}// end for
}
}
执行结果
2020-07-05 11:00:10.287 [pool-1-thread-3] INFO WorkerThread - pool-1-thread-3 Start. Command = cmd5
2020-07-05 11:00:10.287 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 Start. Command = cmd6
2020-07-05 11:00:10.287 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 Start. Command = cmd7
2020-07-05 11:00:10.287 [pool-1-thread-2] INFO WorkerThread - pool-1-thread-2 Start. Command = cmd2
2020-07-05 11:00:10.287 [pool-1-thread-1] INFO WorkerThread - pool-1-thread-1 Start. Command = cmd1
2020-07-05 11:00:10.287 [main ] INFO RejectedExecutionHandlerImpl - [cmd8] is rejected(被拒绝/驳回)
2020-07-05 11:00:10.294 [main ] INFO RejectedExecutionHandlerImpl - [cmd9] is rejected(被拒绝/驳回)
2020-07-05 11:00:10.294 [main ] INFO RejectedExecutionHandlerImpl - [cmd10] is rejected(被拒绝/驳回)
2020-07-05 11:00:10.294 [main ] INFO WorkerPoolApplication - 关闭线程池
2020-07-05 11:00:10.359 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
2020-07-05 11:00:11.364 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
2020-07-05 11:00:12.294 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 End. Command = cmd7
2020-07-05 11:00:12.294 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 End. Command = cmd6
2020-07-05 11:00:12.294 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 Start. Command = cmd3
2020-07-05 11:00:12.294 [pool-1-thread-2] INFO WorkerThread - pool-1-thread-2 End. Command = cmd2
2020-07-05 11:00:12.294 [pool-1-thread-3] INFO WorkerThread - pool-1-thread-3 End. Command = cmd5
2020-07-05 11:00:12.294 [pool-1-thread-1] INFO WorkerThread - pool-1-thread-1 End. Command = cmd1
2020-07-05 11:00:12.294 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 Start. Command = cmd4
2020-07-05 11:00:12.366 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
2020-07-05 11:00:13.370 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
2020-07-05 11:00:14.297 [pool-1-thread-4] INFO WorkerThread - pool-1-thread-4 End. Command = cmd4
2020-07-05 11:00:14.297 [pool-1-thread-5] INFO WorkerThread - pool-1-thread-5 End. Command = cmd3
2020-07-05 11:00:14.374 [Thread-0] INFO MyMonitorThread - poolSize/corePoolSize [0/2] Active: 0, Completed: 7, Task: 7, queue:[],isShutdown: true, isTerminated: true
执行结果解读
首先,工作线程5、6、7和2、1被创建并启动,一共10个任务,但是线程池最大线程数设置的是5
,等待队列大小设置的是2
,剩下的三个线程(8、9、10),被执行拒绝策略。
这时,我们手动调用shutdown()
,尝试关闭线程池。因为还有线程未执行完,等待队列中也有任务,所以线程池会等待事情全部处理好后,再关闭。
这时,通过监控日志,可以发现:
poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
当前池中线程数为5、计划执行的线程数为7,3、4号工作线程在队列中。
接着,7和6执行完毕,队列中的3号任务开始执行,2、5、1也相继执行完毕,队列中的4号任务开始执行。这时再次观察线程池状态:
poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
可以看到,当前池中线程数为2、已完成执行的任务的总数为5,计划执行的线程数为7,队列为空。
继续,4号和3号线程执行完毕,最后查看线程池状态:
poolSize/corePoolSize [0/2] Active: 0, Completed: 7, Task: 7, queue:[],isShutdown: true, isTerminated: true
可以看到,当前池中线程数为0、已完成执行的任务的总数为7,计划执行的线程数为7,队列为空。线程池关闭。
我们发现、超过线程池核心线程数的、小于最大线程数的这部分线程,优先于等待队列中的任务执行。
目录结构
图:本文目录结构
参考资料
线程池_百度百科
让 CPU 告诉你硬盘和网络到底有多慢 | Cizixs Write Here
多线程上下文切换 - 五月的仓颉 - 博客园
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队
环境说明
- java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
- OS:
macOS High Sierra 10.13.4
- 日志:
logback