两阶段线程终止模型
前言引入
线程终止主要应对的场景是一些服务只做阶段性任务的处理,处理完毕就可以终止线程,如项目中有可能使用到的数据迁移服务,使用时间较短,使用完毕就会将线程终止,那么如何去终止一个线程呢?
这时肯定有人会脱口而出stop(),线程Thread提供的一个方法,这个方法确实可以终止线程但是带来的后果就是一剑封喉不给任何机会,一个业务处理到一半直接stop就有可能导致业务错乱,那么如何优雅的终止线程呢?
两阶段终止模型
线程终止最优雅的方式就是采用两阶段终止模型来处理,何为两阶段终止模型呢?结构图如下所示
第一阶段发出终止指令后并不是马上终止,而是要到第二阶段才会终止这是有时间间隔的,那么这里的指令如何理解呢?聊指令之前我们需要回顾下JAVA线程的生命周期,如下所示
线程终止只能从运行态自然程序运行结束或者发生异常才能做到,那么我们需要终止的线程其实可能有两种状态,休眠状态或者运行状态,也就是说我们要终止正在睡眠的程序就要将线程状态由睡眠状态改为运行状态,这个功能就可以使用JAVA Thread类提供的interrupt() 方法去实现,这就是第一阶段的实现。
那么线程处于运行状态了如何去响应中断请求呢?这个一般采用标识位,线程程序在合适的时候会自动校验标识是否中断,如果中断正常结束程序,没有中断继续运行这就是上面提到的第二阶段。
程序模拟两阶段终止模式
代码实现
举例说明,假设现在业务系统需要数据迁移服务提供迁移服务,数据迁移完毕后需要关闭服务
class Migration{
// 核心逻辑是异步执行,不允许多个线程同时执行迁移逻辑
private boolean started = false;
// 数据迁移线程
private Thread rpcThread;
// 启动数据迁移服务
public synchronized void start(){
if (started){
return;
}
started = true;
rpcThread = new Thread(()->{
while (!rpcThread.isInterrupted()){
try {
System.out.println("模拟处理迁移逻辑中。。。。");
Thread.sleep(2000);
// 上报任务
} catch (InterruptedException e) {
// 重新设置标志位
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
// 迁移执行完毕,放开让其它线程进入
started = false;
});
rpcThread.start();
}
// 终止数据迁移服务
public synchronized void stop(){
rpcThread.interrupt();
}
}
注意点
- 如果在中断后线程处于睡眠状态那么会触发中断异常InterruptedException,这个异常会将线程从睡眠状态变为运行状态,会清空中断标识位所以需要重新设置中断标识。
- started标志位是有必要的,同一时间只允许一个线程执行迁移操作,因为迁移操作是启动了一个子线程处理,就是异步执行。
PLUS版本
是不是按照两阶段提交这样做一点问题都没有呢?如果我们在执行迁移任务时,去调用的是第三方SDK,这时发生线程中断,SDK有没有正确处理中断异常我们是不知道的,如果出现异常SDK没有重置线程中断标识那么我们程序不就会造成死循环,所以我们一定要自定义中断标识,代码如下
class MigrationUpgrade{
private boolean started = false;
private Thread rpcThread;
// 线程不调用wait、jion、sleep方法中的一种,是无法响应中断的
// 这个时候基于interrupt的可见性就不成立了,所以工程上这类变量都需要加volatile
private volatile boolean isInterrupted = false;
// 启动
public synchronized void start(){
if (started){
return;
}
started = true;
rpcThread = new Thread(()->{
while (!isInterrupted){
try {
// 模拟迁移调用SDK
System.out.println("处理迁移逻辑中。。。。");
Thread.sleep(2000);
} catch (InterruptedException e) {
// 重新设置标志位 规范处理不保证其它方法调用此逻辑
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
started = false;
});
rpcThread.start();
}
// 终止
public synchronized void stop(){
isInterrupted = true;
rpcThread.interrupt();
}
}
注意点
- 中断标识位isInterrupted是否有必要设置为volatile,因为根据Happens-Before规则,调用interrupt的线程Happens-Before与被中断的线程,也就是说调用stop方法更新isInterrupted的值对新创建的rpcThread线程一定是可见的,但如果线程不调用wait、sleep、join等方法那么正常执行并不会响应中断,Happens-Before规则失效,所以用volatile保证可见性是有必要的。
- catch中重新设置标志位是否有必要呢?对于上面的代码是没必要的,因为不需要再去调用rpcThread.isInterrupted()判断线程是否终止,这个功能由中断标识位isInterrupted代替,这里是为了代码的规范性,不然别的模块调用时就又有可能出现死循环的情况。
线程池终止
终止方案
上面都是聊的线程终止方案,但是现实中使用并不多,最多的还是线程池居多,那么线程池如何优雅的终止呢?
JAVA提供给我们两种解决方案shutdown()和shutdownNow()
- 调用shutdown方法后,线程池将拒绝接收新的任务,会等待线程池中正在执行的线程和阻塞队列中未执行的线程执行完毕后再终止线程池。
- 调用shutdownNow方法后,同样线程池将拒绝接收新的任务,但是不会等待线程池中正在执行的线程和阻塞队列中未执行的线程执行完毕,而是直接终止,当然shutdownNow也是能给出补偿机制的,它将返回阻塞队列中未执行的线程集合
List<Runnable> shutdownNow()
。
代码实现
shutdown
public static void main(String[] args) throws InterruptedException {
// 线程池只有一个线程
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 第一个线程占用线程池中所有的线程
executorService.execute(()->{
try {
System.out.println("第一个线程执行中。。。");
TimeUnit.SECONDS.sleep(20);
System.out.println("第一个线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread.sleep(1000);
// 这是第二个线程
executorService.execute(()->{
System.out.println("第二个线程执行");
});
System.out.println("==准备终止线程了===");
executorService.shutdown();
}
执行结果如下,创建的线程池核心线程数和最大线程数都是1,那么第一个线程就占据了所有的资源,第二个线程在阻塞队列中等待,这时调用shutdown终止线程,程序不会结束而是等待线程池中正在执行的线程和阻塞队列中未执行的线程执行完毕后再终止。
shutdownNow
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 第一个线程占用线程池中所有的线程
executorService.execute(()->{
try {
System.out.println("第一个线程执行中。。。");
TimeUnit.SECONDS.sleep(20);
System.out.println("第一个线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread.sleep(1000);
// 这是第二个线程
executorService.execute(()->{
System.out.println("第二个线程执行");
});
System.out.println("==准备终止线程了===");
List<Runnable> runnables = executorService.shutdownNow();
System.out.println("阻塞队列值:"+runnables.size());
runnables.forEach(runnable -> {
System.out.println("执行======");
runnable.run();
});
}
执行结果如下