Java并发interrupt方法详解

138 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第16天,点击查看活动详情

一、概述

​ 中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序。

二、中断线程方法

interrupt()

描述:该方法用于中断Thread线程,此线程并非当前线程,而是调用interrupt()方法的实例所代表的线程,并不是强制关闭线程,而是将中断标记位设置为true,线程的中断需要在线程内部协作关闭

使用示例
  • 通过interrupt()中断正在休眠的线程
/**
 * @author lilinchao
 * @date 2022-10-08
 * @description 通过interrupt() + InterruptedException来中断线程
 **/
@Slf4j(topic = "c.Test02")
public class Test02 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("start...");
                //线程进入睡眠状态 sleep 5s
                Sleeper.sleep(5);
                log.debug("end...");
            }
        };

        t1.start();

        //主线程睡眠 2s 后运行 t1.interrupt 打断正在睡眠的t1线程
        Sleeper.sleep(2);
        //在线程阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态
        t1.interrupt();
    }
}

运行结果

14:21:10.443 c.Test02 [t1] - start...
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.lilinchao.thread.utils.Sleeper.sleep(Sleeper.java:8)
	at com.lilinchao.thread.demo01.Test02$1.run(Test02.java:19)
14:21:12.457 c.Test02 [t1] - end...

说明

  • 打断正在运行中的线程并不会影响线程的运行,但如果线程监测到了打断标记为true,可以自行决定后续处理。
  • 打断阻塞中的线程(如:线程被Object.wait, Thread.joinThread.sleep三种方法之一阻塞)会让此线程产生一个InterruptedException异常,结束线程的运行。但如果该异常被线程捕获住,该线程依然可以自行决定后续处理(终止运行,继续运行,做一些善后工作等等)
  • 如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException

isInterrupted()

描述:用于判断thread的中断状态,不清除中断状态

使用示例
  • 停止正在运行中的线程
/**
 * @author lilinchao
 * @date 2022-10-08
 * @description 中断正常运行的线程
 **/
@Slf4j(topic = "c.Test09")
public class Test09 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            //无限循环
            while (true){
                Thread current = Thread.currentThread();
                //判断当前线程的中断状态
                boolean interrupted = current.isInterrupted();
                //如果为true表示线程被中断,执行判断条件,通过break语句退出循环
                if (interrupted) {
                    log.debug("打断状态:{}",interrupted);
                    break;
                }
            }
        },"t1");
        t1.start();

        Sleeper.sleep(1);

        t1.interrupt();
    }
}

运行结果

14:43:19.316 c.Test09 [t1] - 打断状态:true

说明

  • 在while()循环中,每次循环,都会通过isInterrupted()方法判断当前线程的中断状态,如果为true表示线程被interrupt()方法中断,执行判断条件,通过break语句退出循环。以此达到终止正在运行线程的目的。
  • 这样做的好处是,即使线程被中断,也不会立即停止该线程的运行,而是可以继续进行后续的善后操作,做到了”优雅的关闭线程“。

interrupted()

描述:该方法为静态方法,判断当前线程的中断状态,并会将中断标记位设置为false,在第二次调用时中断状态会返回false

使用示例
  • 通过interrupted()方法停止正在运行中的线程
/**
 * @author lilinchao
 * @date 2022-10-08
 * @description 中断正常运行的线程
 **/
@Slf4j(topic = "c.Test10")
public class Test10 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            //无限循环
            while (true){
                Thread current = Thread.currentThread();
                //判断当前线程的中断状态
                boolean interrupted = current.interrupted();
                //如果为true表示线程被中断,执行判断条件,通过break语句退出循环
                if (interrupted) {
                    log.debug("打断状态1:{}",interrupted);
                    //再次调用interrupted方法,查看状态标记是否被重置
                    log.debug("打断状态2:{}",current.interrupted());
                    break;
                }
            }
        },"t1");
        t1.start();

        Sleeper.sleep(1);

        t1.interrupt();
    }
}

运行结果

15:11:51.099 c.Test10 [t1] - 打断状态1:true
15:11:51.101 c.Test10 [t1] - 打断状态2:false

说明

  • interrupted()方法返回的是上一次的中断状态,并且会清除该状态,所以连续调用两次,第一次返回true,第二次返回false。

三、两阶段终止模式

Two Phase Termination 在一个线程 T1 中如何“优雅”终止线程 T2?

这里的【优雅】指的是给 T2 一个料理后事的机会。

06.并发编程之interrupt方法详解01.jpg

说明

  • 如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是会抛出异常,此时需要手动设置打断标记为true,阻止线程继续运行;
  • 如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true
  • 处理好这两种情况就可以放心地来料理后事了

错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop方法虽然可以强行终止正在运行或挂起的线程,但使用stop方法是很危险的。stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁。
  • 使用 System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都停止

使用 isInterrupted

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test12")
public class Test12 {
    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt t = new TPTInterrupt();
        t.start();
        //异常打断
        Thread.sleep(3500);
        //正常打断
//        Thread.sleep(1000);
        log.debug("stop");
        t.stop();
    }
}

@Slf4j(topic = "c.TPTInterrupt")
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) {
                    // sleep 打断后会清除打断标记,所以要重新设置
                    current.interrupt();
                    e.printStackTrace();
                }
                // 执行监控操作
            }
        },"监控线程");
        thread.start();
    }
    public void stop() {
        thread.interrupt();
    }
}

运行结果

17:45:27.063 c.TPTInterrupt [监控线程] - 将结果保存
17:45:28.076 c.TPTInterrupt [监控线程] - 将结果保存
17:45:29.089 c.TPTInterrupt [监控线程] - 将结果保存
17:45:29.557 c.Test12 [main] - stop
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.lilinchao.thread.demo01.TPTInterrupt.lambda$start$0(Test12.java:35)
	at java.lang.Thread.run(Thread.java:748)
17:45:29.558 c.TPTInterrupt [监控线程] - 料理后事

使用停止标记

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test11")
public class Test11 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        tpt.start();
        tpt.start();

        Thread.sleep(3500);
        log.debug("停止监控");
        tpt.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{
    // 监控线程
    private Thread monitorThread;
    // 停止标记
    private volatile boolean stop = false;
    // 判断是否执行过 start 方法
    private boolean starting = false;

    // 启动监控线程
    public void start(){
        synchronized (this) {
            if (starting) { // false
                return;
            }
            starting = true;
        }

        monitorThread = new Thread(() -> {
            while (true) {
//                Thread current = Thread.currentThread();
                // 是否被打断
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    // 停止监控线程
    public void stop() {
        stop = true;
        monitorThread.interrupt();
    }
}

运行结果

17:47:34.749 c.TwoPhaseTermination [monitor] - 执行监控记录
17:47:35.757 c.TwoPhaseTermination [monitor] - 执行监控记录
17:47:36.769 c.TwoPhaseTermination [monitor] - 执行监控记录
17:47:37.239 c.Test11 [main] - 停止监控
17:47:37.239 c.TwoPhaseTermination [monitor] - 料理后事

说明

  • 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性

四、不推荐的方法

有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名static功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行