Java 并发编程 02:优雅停止线程

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

简介

我们可以调用线程对象的 start() 方法启动线程,而停止线程则有些复杂。

通常情况下,我们不会手动停止一个线程,而是允许线程运行到整个进程结束,然后让它自然停止。但是对于某些特殊情况,需要提前停止线程,比如用户突然关闭程序、程序运行出错等。此时即将停止的线程在很多业务场景下仍然很有价值,但是 Java 并没有提供简单易用,能够直接安全停止线程的能力。

对于 Java 而言,最正确的停止线程的方式是使用 interrupt(),但它仅仅起到通知被停止线程的作用。对于被停止的线程而言,它拥有完全的自主权,既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。

为什么 Java 不提供强制停止线程的能力?

  • Java 希望程序间能够相互通知、相互协作地管理线程
  • 因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题
  • 比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据错乱,不管是中断命令发起者,还是接收者都不希望数据出现问题

停止线程的错误方式

以下三种停止线程的方式被标记为已过时 @Deprecated

  • stop():停止线程运行。stop() 会直接把线程停止,虽然它会释放锁,但是会导致任务戛然而止,没有给线程足够的时间处理收尾工作,可能会出现数据完整性等问题。
  • suspend() / resume():挂起线程运行 / 恢复线程运行。线程调用 suspend() 后,不会释放锁。在该线程被 resume() 之前,这把锁不会被释放。这样就可能导致死锁问题。
  • 借助 volatile 标记位

为什么借助 volatile 标记位停止线程是错误的?

在一些情况下,volatile 标记位可以正常停止线程,如下:

public class VolatileStopThread implements Runnable {
    private volatile boolean canceled = false;
	
    @Override
    public void run() {
        int num = 0;
        try {
            while (!canceled && num <= 1000000) {
                if (num % 10 == 0) {
                    System.out.println(num + "是10的倍数");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileStopThread r = new VolatileStopThread();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(3000);
        r.canceled = true;
	}
}
复制代码

但是,如果被停止的线程在执行过程中被阻塞了(比如往已经放满的阻塞队列中添加元素),那么它将无法检测到 volatile 标记位已经被设置为 false,从而无法跳出循环。所以不能借助 volatile 标记位来停止线程。

interrupt() 方法与中断标记位

调用线程的 interrupt() 方法,可以停止线程,在这里分为两种情况:

  • 调用运行中线程的 interrupt() 方法,会设置中断标记位。
  • 调用阻塞中线程的 interrupt() 方法,比如调用了 sleep()wait()join() 方法的线程,会抛出 InterruptedException 异常,同时清除中断标记,也就是将其设置为 false

访问中断标记位有哪些方法?

  • isInterrupted():该方法不会影响中断标记。
  • 静态方法 Thread.interrupted():返回中断标记后,会将其设置为 false。也就是说,该方法可以用于清空中断标记位。

正确停止线程

方式一:在方法签名中声明抛出异常,在 run() 方法中使用 try/catch 捕获异常。

  • throws InterruptedException 标记方法,不捕获异常,以便该异常可以传递到顶层的 run() 方法
  • 由于 run() 方法无法抛出受检异常,所以只能使用 try/catch 捕获异常,这样的好处在于,顶层方法必须处理该异常

方式二:在 catch 块中再次中断线程。

  • catch 块中调用 Thread.currentThread().interrupt() 函数
  • 因为如果线程在休眠期间被中断,那么会自动清除中断信号
  • 这时如果手动添加中断信号,中断信号仍然可以被捕捉到

如果盲目地忽略中断请求,也就是屏蔽中断信号,可能会导致线程无法被正确停止。

两阶段终止模式

上面的方式二其实就是两阶段终止模式

两阶段终止模式 Two-Phase Termination Pattern 是一种多线程设计模式,用于在线程 t1 中优雅地停止另一个线程 t2,“优雅”指的是给被停止的线程做收尾工作的机会。

两阶段终止分为两种中断情况:

  • 运行时中断,打断标记已经为 true,只需要检测即可
  • 睡眠时中断,需要捕捉异常,以及重新设置打断标记为 true

团子注:使用 interrupt() 实现两阶段终止。关键点在于对于 sleepjoin 或者 wait 等处于阻塞状态的线程,interrupt() 只会抛出 InterruptedException,而不会设置打断标记,所以只要在异常处理内容中手动再次 interrupt() 重新设置打断标记即可。

流程图如下:

202204041124 Java 并发编程 02:优雅停止线程 00.png

示例代码如下:

class TPTInterrupt {
	private Thread thread;
	
	public void start(){
		thread = new Thread(() -> {
			while(true) {
				Thread current = Thread.currentThread();
				if(current.isInterrupted()) {
					log.debug("料理后事");
					break;
				}
				try {
					Thread.sleep(1000);
					log.debug("将结果保存");
				} catch (InterruptedException e) {
					current.interrupt();
				}
				// 执行监控操作 
			}
		}, "监控线程");
		thread.start();
	}
	
	public void stop() {
		thread.interrupt();
	}
}
复制代码

调用:

TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop()
复制代码

结果:

11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop 
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
复制代码

参考资料

  • 拉勾:Java 并发编程 78 讲第 2
  • [[202112052113 Java 多线程:interrupt 方法]]
  • [[202112052155 Java 多线程设计模式:两阶段终止]]
  • [[202112052217 Java 多线程:不推荐使用的过时方法]]

猜你喜欢

转载自juejin.im/post/7083519665414078471
今日推荐