📢CSDN博客主页:低山高梧桐-致力于做最优质的内容
📢如果涉及到版权问题,烦请联系作者删除!
📢如果文章有谬误,烦请您指出斧正,作者致力于做最好的博客。
📢整合:低山高梧桐 首发于CSDN 欢迎点赞👍收藏⭐留言打扰📝
看到线程池这个主题,第一次首先想到的是
为什么会有线程池呢?
我们平时调用线程,需要创建一个线程,并在结束时销毁。
这就是一个线程最简单的两个状态,但是像这样的频繁新建和销毁会带来极大的开销。
有请电科大的工学硕士现身说法:
对操作系统来说,创建一个线程的代价是十分昂贵的, 需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。【分配内存、列入调度、内存换页、清空缓存和重新读取】
试想一下,如果有大量的用户申请创建线程,内存是不是会瑟瑟发抖呢?
如果由我们来着手解决这个线程开销大的问题。
你会怎么解决呢?
线程池的出现
首先我们想到的当然是复用,毕竟一入Java深似海,从此耦合是路人。
我们工作中学习中最常打交道的优化方法就是复用。
将影响多个对象的公共代码提取为公共的模块或者工具类也是复用的一种。
所以线程可不可以拿来复用呢?在需要的时候直接调度就好了吧?
当然了,将一些已经创建好的线程直接拿来复用,再由一个统一的组件负责调配管理这些线程。
恭喜你,这个组件可以称之为一半线程池,为什么是一半呢?
因为线程池的线程是有数量的,如果超过限制了在条件允许的情况下还是要新建的吧?
大量的用户:我又来了!
内存:淦!
聪明的你想到了线程还需要一个限制,用来限制线程的数量防止过多。
那问题又来了,怎么限制线程的数量过多呢?
当线程不够分配的时候,有没有防止超过线程数量的手段呢?
- 丢任务,丢弃最新的任务或者最老的任务
- 谁提交的任务谁执行
- 抛个异常
上面的分析思路叫做池化管理思想,想想我们研究的这个线程池有什么好处:
- 降低系统开销,不用频繁新建销毁线程了
- 提高响应速度,所有的线程都是分配好的,直接拿来就用
- 提高系统资源可管理性,由线程池统一对线程进行调配
线程池工作原理
上面提到,线程池是管理线程的组件。那线程数量怎么保证呢?
为了保证线程池中线程数量的稳定,我们决定向其中加入一个核心线程数的设定:
当线程池中的线程没有达到核心线程数的时候就新建线程,当线程池中的线程超过核心线程数的时候就添加任务,给任务分配线程
任务也需要管理呀,我们再让任务排个队吧!就叫做任务队列
还记得刚才提到的防止超过线程数量的手段
吗?当线程池线程都处于运行状态,任务队列也都满了的时候,我们需要拒绝新的任务进入,这个就叫拒绝策略吧
线程池核心属性 / 线程池七个参数
至此,我们的线程池已经是有模有样了,因为最核心的东西已经集齐了:
- corePoolSize(核心线程数):当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态
- workQueue(队列):用于保留任务并移交给工作线程的阻塞队列
- handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:
a. 线程池运行状态不是 RUNNING;
b. 线程池已经达到最大线程数,并且阻塞队列已满时
这只是最核心的,我们还需要新建线程的工厂,防止线程溢出的最大线程值,以及用来消灭超过核心线程数的额外线程的保持存活时间。时间还需要一个单位:
- threadFactory(线程工厂):用于创建工作线程的工厂
- maximumPoolSize(最大线程数):线程池允许开启的最大线程数
- keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止
- TimeUnit(时间单位):空闲线程存活时间的描述单位
线程池执行流程
如果有任务来了,我们怎么办呢?
先从核心的三个参数入手吧
首先是不是需要先判断一下当前的工作线程是不是大于核心线程数?小于直接创建线程运行
大于的话,再判断一下任务队列满了没有?没满就排排队
满了的话,最后再判断有没有超过最大线程数?没超过就创建线程执行吧(注意这里的线程是超过核心线程数的,但是没有达到最大线程数)
超过了的话,直接执行拒绝策略拒绝掉就好了吧
那画个流程图岂不是更直观?
流程清楚了,要不看看源代码?
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当前工作的线程数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 创建新的线程执行此任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 检查线程池是否处于运行状态,如果是则把任务添加到队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次检线程池是否处于运行状态,防止在第一次校验通过后线程池关闭
// 如果是非运行状态,则将刚加入队列的任务移除
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果线程池的线程数为 0 时(当 corePoolSize 设置为 0 时会发生)
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 新建线程执行任务
}
// 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
else if (!addWorker(command, false))
// 执行拒绝策略
reject(command);
}
拒绝策略
还记得这个吗?
当线程不够分配的时候,有没有防止超过线程数量的手段呢?
- 丢任务,丢弃最新的任务或者最老的任务
- 谁提交的任务谁执行
- 抛个异常
这里已经列出了拒绝策略的全部内容了
当然,我们要整理一下:
- AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
- CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行;
- DiscardPolicy:忽略此任务,忽略最新的一个任务;
- DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务。
但是线程池总不能自己随机选择拒绝策略吧?
设置一下默认拒绝策略。
默认的拒绝策略为 AbortPolicy 中止策略。
就像steam中的创意工坊一样,四种策略不可能满足所有情况
为此,我们也加一个“创意工坊”,就叫做自定义拒绝策略:
- 自定义拒绝策略:除了 JDK 提供的四种拒绝策略之外,我们还可以实现通过 new RejectedExecutionHandler,并重写 rejectedExecution 方法来实现自定义拒绝策略
如何关闭线程池?
正如世界上没有永动机,打工人受不了007一样,线程池可能也需要休息
开个玩笑,线程池怎么会休息呢?它干不了总有别的线程池来干
但是我们要在不需要线程池的时候关闭它
线程池是关闭了,但是其中没有执行完的线程怎么办呢?
是不是要让剩下的线程全部执行完毕呢?
那要不要等待线程池执行完呢?
我能不能中间打断线程池运行呢?
聪明的你为了区别不同的停止场景,设计了两种方案
- 调用方法停止线程池:
- 调用线程池的 shutdown() 方法来关闭线程池。该方法会停止线程池的接受新任务,并尝试将所有未完成的任务完成执行;
- 调用线程池的 shutdownNow() 方法来关闭线程池。该方法会停止线程池的接受新任务,并尝试停止所有正在执行的任务。该方法会返回一个未完成任务的列表,这些任务将被取消。
- 等待线程池停止:在关闭线程池后,通过调用 awaitTermination() 方法来等待所有任务完成执行。该方法会阻塞当前线程,直到所有任务完成执行或者等待超时。
如何判断线程池的任务已经执行完?
如题,关闭线程池的方案已经告一段落
值得注意的是,我们需要等到线程执行完毕之后,才能对于数据进行封装和返回
但是如何判断线程池的任务已经执行完了呢?
还记得任务队列吗?可不可以统计一下完成的线程,并与总任务数量进行对比来判断呢?
或者在创建任务的时候就调用一个我们封装好的方法,在里面封装上状态变量,在返回的时候就能根据状态变量判断是否完成呢?
或者设置一个“障碍”当所有线程执行完的时候再打开“障碍”,执行后面的操作。
等等
整理一下:
- 使用 getCompletedTaskCount() 统计已经执行完的任务,和 getTaskCount() 线程池的总任务进行对比,如果相等则说明线程池的任务执行完了,否则既未执行完。
- 使用 FutureTask 等待所有任务执行完,线程池的任务就执行完了。
FutureTask内部封装了一个状态变量,用于记录任务的状态(等待、运行、完成、取消等),以及计算结果或异常信息。通过该状态变量,我们可以判断任务是否已完成、获取任务的执行结果等。 - 使用 CountDownLatch 或 CyclicBarrier 等待所有线程都执行完之后,再执行后续流程。
这是最常用的,实际上不止这些。
参考&致谢
本文暂时为试读版,因为作者才疏学浅,后续会对本文不断补充
Java 全栈知识点问题汇总(上)
如何判断线程池任务已执行完? | Javaᶜⁿ 面试突击
Java的CountDownLatch和CyclicBarrier的理解和区别_java countdownlatch和barrier区别-CSDN博客
深入理解Java中的FutureTask:原理、用法和最佳实践 - 掘金
线程创建的开销与线程池
面试必问的线程池,你懂了吗?