【并发编程系列】
❤️并发编程❤️创建线程的四种方式 线程通信
❤️并发编程❤️一万字线程生命周期和状态转换知识梳理
❤️并发编程❤️Java内存模型
❤️并发编程❤️重排序与happens-before
❤️并发编程❤️显式锁Lock和内置锁知识整理
❤️并发编程❤️如何正确停止线程?
❤️并发编程❤️生产者消费者模式实现的三种方式
❤️并发编程❤️线程安全问题分析和场景总结
❤️并发编程❤️线程池源码分析以及线程复用的原理
❤️并发编程❤️如何正确关闭线程池?shutdown 和 shutdownNow 的区别?
在 ThreadPoolExecutor 中涉及关闭线程池的方法,如下所示。
(0)测试线程任务代码
import lombok.SneakyThrows;
import java.util.concurrent.atomic.AtomicInteger;
/***
* 测试线程任务
* @author ZhangYu
* @date 2021/10/5
*/
public class MyTask implements Runnable{
private final static AtomicInteger count=new AtomicInteger();
private final String name;
public MyTask(String name) {
this.name = name;
}
@SneakyThrows
@Override
public void run() {
Thread.sleep(500);
System.out.println("当前任务名称=="+name+"计数=="+count.incrementAndGet());
}
}
(1)shutdown()
shutdown可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。但这并不代表 shutdown() 操作是没有任何效果的,调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。
public static void main(String[] args) {
ThreadPoolExecutor service = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(20));
for (int i = 0; i < 5; i++) {
service.execute(new MyTask("编号"+i));
}
service.shutdown();
service.execute(new MyTask("测试"));
}
上面创建一个线程池,其中线程池数为1,任务队列20,然后创建了五个任务并执行,接着调用的线程池停止方法service.shutdown();,并在停止后又添加了一个任务。
1:通过结果可以看到在线程池关闭后再添加任务时抛出了RejectedExecutionException异常
2:通过控制台可以看到上面的任务以及在阻塞队列中的任务都被执行了,说明shutdown会在执行完正在运行的以及任务队列任务后再关闭
(2)isShutdown()
isShutdown()可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 或者 shutdownNow 方法。这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。
因为任务的run方法中增加了 Thread.sleep(500);任务会消耗一些时间,在执行了shutdown()方法后再执行isShutdown()后可以看到返回的是true,并且此时任务还处于运行中,说明这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。
(3)isTerminated()
isTerminated()可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了,因为我们刚才说过,调用 shutdown 方法之后,线程池会继续执行里面未完成的任务,不仅包括线程正在执行的任务,还包括正在任务队列中等待的任务。比如此时已经调用了 shutdown 方法,但是有一个线程依然在执行任务,那么此时调用 isShutdown 方法返回的是 true ,而调用 isTerminated 方法返回的便是 false ,因为线程池中还有任务正在在被执行,线程池并没有真正“终结”。直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。
通过上面的代码运行结果可以看到在第一个执行 isTerminated()时并没有返回false,因为此时虽然执行了关闭方法但是没有任务还没有执行结束。在等待了5s后此时任务已经执行结束了,再次运行 isTerminated()可以看到已经返回了true,直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。
(4)awaitTermination()
awaitTermination()本身并不是用来关闭线程池的,而是主要用来判断线程池状态的。比如我们给 awaitTermination 方法传入的参数是 10 秒,那么它就会陷入 10 秒钟的等待,直到发生以下三种情况之一:
【1】等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true;
【2】等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false;
【3】等待期间线程被中断,方法会抛出 InterruptedException 异常。
也就是说,调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle。
我们则可以根据 awaitTermination() 返回的布尔值来判断下一步应该执行的操作。
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor service = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(20));
for (int i = 0; i < 5; i++) {
service.execute(new MyTask("编号"+i));
}
service.shutdown();
System.out.println(service.awaitTermination(1,TimeUnit.MILLISECONDS));
System.out.println(service.awaitTermination(5000,TimeUnit.MILLISECONDS));
}
上面代码执行了俩次awaitTermination,第一个等待1毫秒返回false,第二次等待了5秒,返回了true。
(5)shutdownNow()
shutdownNow()表示立刻关闭的意思。在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案并在后期重试。shutdownNow() 的源码如下所示。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
其中interruptWorkers()代码会让每一个已经启动的线程都中断,这样线程就可以在执行任务期间检测到中断信号并进行相应的处理,提前结束任务。这里需要注意的是,由于 Java 中不推荐强行停止线程的机制的限制,即便我们调用了 shutdownNow 方法,如果被中断的线程对于中断信号不理不睬,那么依然有可能导致任务不会停止。可见我们在开发中落地最佳实践是很重要的,我们自己编写的线程应当具有响应中断信号的能力,应当利用中断信号来协同工作。
这里修改测试任务MyTask的代码,手动捕获中断异常并输出信息,以此感应是否接收到中断异常
public static void main(String[] args) {
ThreadPoolExecutor service = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(20));
for (int i = 0; i < 5; i++) {
service.execute(new MyTask("编号"+i));
}
List<Runnable> runnables = service.shutdownNow();
System.out.println("正在等待的任务数量=="+runnables.size());
System.out.println("手动执行返回的未完成任务");
for (Runnable runnable : runnables) {
new Thread(runnable).start();
}
}