文章目录
前言
再看本文之前,强烈建议先查看上篇【Java多线程】自己实现一个简单的线程池(一)该文章主要实现了线程池的以下功能
- 线程池基本调度功能。
- 线程池自动扩容缩容。
- 队列缓存线程。
- 关闭线程池。
这些功能,最后也留下了三个待实现的 features 。
- 执行带有返回值的线程。
- 异常处理怎么办?
- 所有任务执行完怎么通知我?
自定义任务完成后的通知
大家在用线程池的时候或多或少都会有这样的需求:
线程池中的任务执行完毕后再通知主线程做其他事情,比如一批任务都执行完毕后再执行下一波任务等等。
如: 往线程池中提交了 13 个任务,直到所有任务都执行完毕后再打印
“所有任务执行完毕”
这个日志。
执行结果如下:
为了简单的达到这个效果,我们可以在初始化线程池的时候传入一个Nofify接口
的实现,这个接口就是用于任务完成之后的回调。
Notify接口只有一个方法
public interface Notify {
/**
* 回调函数
*/
void notifyListen();
}
实现原理
想要实现这个功能的关键是在什么时候回调这个接口的实现?
- 仔细想想其实也简单:只要我们
记录提交到线程池中的任务
及完成的数量
,他们两者的差为 0
时就认为线程池中的任务已执行完毕;
这时便可回调这个接口
所以在往线程池中写入任务时我们原子记录任务数量+1(原子递增)
为了并发安全的考虑,这里的计数器采用了原子类 AtomicInteger 。
/**
* 提交到线程池的任务总数计数器
*/
private AtomicInteger totalTask = new AtomicInteger();
线程池中的任务执行完成后递减任务数量-1:(原子递减)
每次任务执行完毕后判断计数器是否为 0
,为0时 表示线程池所有任务执行完毕;这时便可回调我们自定义的接口完成通知。
JDK 的实现
在 jdk 中的java.util.concurrent.ThreadPoolExecutor
中也有相关的 API ,只是用法不太一样,但本质原理都大同小异。
使用 ThreadPoolExecutor 的常规关闭流程如下:
执行结果
线程提交完毕后执行shutdown()
关闭线程池,接着循环调用awaitTermination()
方法,一旦任务全部执行完毕后则会返回 true 从而退出循环。
这两个方法的目的和原理如下:
- 执行
shutdown()
后会将线程池的状态置为关闭状态
,这时将会停止接收新的任
务同时会等待队列中的任务全部执行完毕
后才真正关闭线程池。 awaitTermination
会阻塞
直到线程池所有任务执行完毕或者超时时间已到
。(true表示所有任务执行完毕或者超时时间已到,false还有任务正在运行或者等待运行)
为什么要两个 api 结合一起使用呢?
主要还在最终的目的是:所有线程执行完毕后再做某件事情,也就是在线程执行完毕之前其实主线程是需要被阻塞的。
shutdown()
执行后并不会阻塞,会立即返回,所有才需要后续用循环不停的调用 awaitTermination()
,因为这个 api 才会阻塞线程。- 其实我们查看源码会发现,
ThreadPoolExecutor
中的阻塞
依然也是等待通知机制
的运用,只不过用的是LockSupport 的 API
而已。
实现带有返回值的线程
如: 需要线程异步计算某些数据然后得到结果最终汇总使用。
- 首先
任务Worker
是不能实现Runnable
接口了,它的run()
是没有返回值的;所以我们改成实现一个 Callable 的接口,他的call()
是可以带返回值的
2. 在提交任务时也稍作改动
- 执行任务的函数由
execute()
换为了submit()
,同时submit()
会返回一个返回值Future
- 保存每次提交任务返回的
Future
- 循环获取提交的
Future.get()
方法获取返回值
执行结果
实现原理
再看具体实现之前先来思考下这样的功能如何实现?
-
受限于 jdk 的线程 api 的规范,要执行一个线程不管是实现
runnable接口
还是继承Thread类
,最终都是执行的run() 函数
。 -
所以我们想要一个线程有返回值无非只能是
在执行 run() 函数时去调用一个有返回值的方法
,再将这个返回值存放起来用于后续使用。
1.新建了一个 Callable<T>
的接口(和*java.util.concurrent.Callable
*一样),它的call()
就是刚才提到的有返回值的方法,所以我们应当在线程的 run() 函数
中去调用它。
2.新建一个Future
的接口,他的主要作用是获取线程的返回值
,也就是 将这个返回值存放起来用于后续使用
3.创建一个 FutureTask
,它实现了 Future 接口
用于后续获取返回值。同时实现了 Runnable 接口
会把自己变为一个线程。
在
FutureTask的 run()
函数中会调用刚才提到的具有返回值的call()
。
4.结合CustomThreadPool
的 submit() 提交任务
和 get() 获取返回值的源码
来看会更加理解这其中的门道。
submit()
非常简单,将我们传进来的Callable 对象
转换为一个FutureTask 对象
,然后再调用之前的 execute()
来丢进线程池(后续的执行就和一个普通的线程进入线程池的流程一样)。
FutureTask实现了Runnable接口
本身也是线程,所以可以直接使用execute()
函数。
5.而future.get()
的真正实现是FutureTask
,所以我们直接看其中的源码就好。
-
get() 在线程没有返回之前是一个
阻塞函数
,最终也是通过notify.wait()
使线程进入阻塞状态来实现的。 -
使其从
wait()
中返回的条件必然是在线程执行完毕拿到返回值的时候才进行唤醒。
一旦线程执行完毕
( callable.call())就会唤醒 notify 对象
,这样 get 方法也就能返回了。
同理x ThreadPoolExecutor
中的原理也是类似,只不过它考虑的细节更多所以看起来很复杂,但精简代码后核心也就是这些。甚至最终使用的 api 看起来都是类似的:
异常处理
最后一个是一些新手使用线程池很容易踩坑
的一个地方:那就是异常处理
。
比如类似于这样的场景:
上图创建只有一个线程的线程池,当循环到第1000次时抛出一个空指针异常
,但这个异常又没有被捕获结果线程既没有继续运行同时线程池也没有退出,会一直卡在这里。
当我们dump 线程快照
会发现:
-
这时线程池中还有一个线程在运行,通过线程名称会发现这是新创建的一个线程(
之前是 Thread-0,现在是 Thread-1
)。 -
它的线程状态为
WAITING
,通过堆栈
发现是卡在了 CustomThreadPool.java:的140处的task()
处。
-
就是卡在了从
队列里获取任务的地方
,由于此时的任务队列是空的,所以他会一直阻塞在这里。
-
其实在
线程池内部
会对线程的运行捕获异常
,但它并不会处理,只是用于标记是否执行成功;
- 一旦执行失败则会
回收掉当前异常的线程
,然后重新创建一个新的 Worker 线程
继续从队列里取任务然后执行。所以最终才会卡在从队列中取任务处
。(其实 ThreadPoolExecutor 的异常处理也是类似的)
接着上面finally后的代码
- 一旦执行失败则会
所以我们在使用线程池时,其中的任务一定要做好异常处理。
总结
这一波下来我觉得线程池搞清楚没啥问题了,总的来看它内部运用了非常多的多线程解决方案,比如:
- ReentrantLock 重入锁来保证线程写入的并发安全。
- 利用等待通知机制来实现线程间通信(线程执行结果、等待线程池执行完毕等)。
最后也学会了:
- 标准的线程池关闭流程。
- 如何使用有返回值的线程。
- 线程异常捕获的重要性。
本文所有源码: 点击这里
《Java动手撸源码》手写实现线程池 这也是一篇好文章