Java中线程的中断(Interrupt)

145 阅读9分钟

什么是线程中断

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

InterruptedException

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。

public class InterruptExample {

    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main$0(InterruptExample.java:5)
    at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:745)

interrupted()

如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Thread thread2 = new MyThread2();
    thread2.start();
    thread2.interrupt();
}
Thread end

线程中断的作用

线程中断是一种线程交互的方式

外界向线程请求中断,线程收到中断请求后进行中断逻辑处理(中断逻辑中可以对该中断请求进行处理,是继续运行还是结束运行)

  • 线程的thread.interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。
  • 线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。
  • 不会像stop方法那样会中断一个正在运行的线程。

比如一个线程正在执行任务,用户点击了终止任务执行,通过向线程发起中断请求,线程收到中断请求后,终止任务执行。

public class ThreadTest {
    public static Logger logger = LoggerFactory.getLogger(ThreadTest.class);

    public static void main(String[] args) {
        thread_interrupt();
    }

    public static void thread_interrupt() {
        Thread t1 = new Thread(
                () -> {
                    logger.info("t1线程开始运行");
                    while (true) {
                        if (Thread.currentThread().isInterrupted()) {
                            logger.info("t1线程收到中断请求");
                            logger.info("t1执行中断逻辑");
                            logger.info("t1退出执行");
                            break;
                        }
                    }
                    logger.info("t1线程结束运行");
                }, "t1"
        );
        t1.start();

        // 主线程休眠5s
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 主线程发起中断请求
        logger.info("主线程发起中断请求");
        t1.interrupt();
    }
}

result:

23:27:05.400 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1线程开始运行
23:27:10.402 [main] INFO com.spring.mybatis.demo.ThreadTest - 主线程发起中断请求
23:27:10.402 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1线程收到中断请求
23:27:10.402 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1执行中断逻辑
23:27:10.402 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1退出执行
23:27:10.402 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1线程结束运行

通过上述代码可以证明收到中断请求后,控制线程中断,倘若收到中断请求后不做任何处理怎么做呢

public class ThreadTest {
    public static Logger logger = LoggerFactory.getLogger(ThreadTest.class);

    public static void main(String[] args) {
        thread_interrupt();
    }

    public static void thread_interrupt() {
        Thread t1 = new Thread(
                () -> {
                    logger.info("t1线程开始运行");
                    while (true) {
                        if (Thread.interrupted()) {
                            logger.info("t1线程收到中断请求");
                            logger.info("t1执行中断逻辑");
                            logger.info("t1线程继续运行。。。");
                        }
                    }
                    logger.info("t1线程结束运行");
                }, "t1"
        );
        t1.start();

        // 主线程休眠5s
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 主线程发起中断请求
        logger.info("主线程发起中断请求");
        t1.interrupt();
    }
}
23:25:00.937 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1线程开始运行
23:25:05.939 [main] INFO com.spring.mybatis.demo.ThreadTest - 主线程发起中断请求
23:25:05.940 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1线程收到中断请求
23:25:05.940 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1执行中断逻辑
23:25:05.940 [t1] INFO com.spring.mybatis.demo.ThreadTest - t1线程继续运行,对中断逻辑

可以发现线程一致在执行

可以发现代码一和代码二的区别代码一使用的是Thread.currentThread().isInterrupted()代码二是用的是Thread.interrupted(),如果代码二使用 Thread.currentThread().isInterrupted()则每次都会支持while中的代码。原因在于

  • Thread.currentThread().isInterrupted():Thread实例方法,用于获取当前线程的中断标记。

  • Thread.interrupted():Thread静态方法,用于获取当前线程的中断标记,并且会清除中断标记。

判断线程是否被中断

通过上文可以发现,判断某个线程是否已被发送过中断请求,需要使用Thread.currentThread().isInterrupted()方法因为使用这个方法进行判断的时候不会清除中断标示位,即不会将中断标设置为false,而不要使用thread.interrupted()方法来判断,原因是该方法调用后会将中断标示位清除,即重新设置为false。

线程中断的主要方法:
  • public void interrupt():Thread实例方法,用于设置中断标记,但是不能立即中断线程。

  • public boolean isInterrupted():Thread实例方法,用于获取当前线程的中断标记。

  • public static boolean interrupted():Thread静态方法,用于获取当前线程的中断标记,并且会清除中断标记。

如何中断线程

线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态。如果一个线程处于了阻塞状态,在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序有足够的时间来处理中断请求。

synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断。与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。你也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。

主要想表达,线程会被提前唤醒,并去执行catch代码块中的逻辑

没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。某些线程非常重要,以至于它们不理会中断,而是在处理完抛出的异常之后继续执行。

public void run() {
    try {
        ...
        /*
         * 不管循环里是否调用过线程阻塞的方法如sleep、join、wait,这里还是需要加上
         * !Thread.currentThread().isInterrupted()条件,虽然抛出异常后退出了循环,显
         * 得用阻塞的情况下是多余的,但如果调用了阻塞方法但没有阻塞时,这样会更安全、更及时。
         */
        while (!Thread.currentThread().isInterrupted()&& more work to do) {
            do more work 
        }
    } catch (InterruptedException e) {
        //线程在wait或sleep期间被中断了
    } finally {
        //线程结束前做一些清理工作
    }
}

sleep()中断

线程休眠后如何唤醒线程

  1. 休眠时间过后自动恢复运行
  2. 线程休眠过程中通过线程中断唤醒
    public static void test_sleep_interrupt() {
        Thread t2 = new Thread(() -> {
            while (true) {
                logger.info("线程开始运行");
                logger.info("线程休眠20s");
                try {
                    TimeUnit.SECONDS.sleep(20);
                } catch (InterruptedException e) {
                    logger.info("收到中断请求....");
                    logger.info("退出");
                    break;
                }
            }
        }, "sleep_interrupt");
        t2.start();
        try {
            logger.info("主线程休眠2s");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.interrupt();
    }
23:51:10.445 [main] INFO com.spring.mybatis.demo.ThreadTest - 主线程休眠2s
23:51:10.445 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 线程开始运行
23:51:10.448 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 线程休眠20s
23:51:12.453 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 收到中断请求....
23:51:12.453 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 退出

该段代码是存在逻辑问题的,比如该线程打开了一个文件那么在退出的时候应该把文件close掉的,那么就需要在退出之前做一些操作。

    public static void test_sleep_interrupt_improve() {
        Thread t2 = new Thread(() -> {
            while (true) {
                if (Thread.interrupted()){
                    logger.info("线程收到中断请求");
                    logger.info("执行清理工作");
                    break;
                }
                logger.info("线程开始运行");
                logger.info("线程休眠20s");
                try {
                    TimeUnit.SECONDS.sleep(20);
                } catch (InterruptedException e) { // 休眠过程中发生中断被捕获到,并清除中断状态
                    logger.info("收到中断请求....,thread interrupter status={}",Thread.currentThread().isInterrupted());
                    // 重新标记该线程被中断
                    Thread.currentThread().interrupt();
                    logger.info("重新标记后....,thread interrupter status={}",Thread.currentThread().isInterrupted());
                }
            }
        }, "sleep_interrupt");

        t2.start();
        try {
            logger.info("主线程休眠2s");
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.interrupt();
    }

thread interrupter status 状态的判断不可以使用Thread.interrupted(),而是要使用Thread.currentThread().isInterrupted()。原因在于Thread.interrupted()用于获取当前线程的中断标记,并且会清除中断标记。

00:19:54.136 [main] INFO com.spring.mybatis.demo.ThreadTest - 主线程休眠2s
00:19:54.136 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 线程开始运行
00:19:54.139 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 线程休眠20s
00:19:56.143 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 收到中断请求....,thread interrupter status=false
00:19:56.146 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 重新标记后....,thread interrupter status=true
00:19:56.146 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 线程收到中断请求
00:19:56.146 [sleep_interrupt] INFO com.spring.mybatis.demo.ThreadTest - 执行清理工作

Stop()为什么被废弃

    /**
     * .....
     * @deprecated This method is inherently unsafe.  Stopping a thread with
     *       Thread.stop causes it to unlock all of the monitors that it
     *       has locked (as a natural consequence of the unchecked
     *       <code>ThreadDeath</code> exception propagating up the stack).  If
     *       any of the objects previously protected by these monitors were in
     *       an inconsistent state, the damaged objects become visible to
     *       other threads, potentially resulting in arbitrary behavior.  Many
     *       uses of <code>stop</code> should be replaced by code that simply
     *       modifies some variable to indicate that the target thread should
     *       stop running.  The target thread should check this variable
     *       regularly, and return from its run method in an orderly fashion
     *       if the variable indicates that it is to stop running.  If the
     *       target thread waits for long periods (on a condition variable,
     *       for example), the <code>interrupt</code> method should be used to
     *       interrupt the wait.
     *       For more information, see
     *       <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">Why
     *       are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
     */
    @Deprecated
    public final void stop() {

从注释可以看出使用Thread.stop()停掉一个线程将会导致所有已锁定的监听器被解锁(解锁的原因是当threaddeath异常在堆栈中传播时,监视器被解锁),这个之前被监听器锁定的对象被解锁,其他线程就能随意操作这个对象,将导致任何可能的结果。

官方给出的网页说明了不能捕获ThreadDeath异常并修复对象的原因:

  1. 一个线程几乎可以在任何地方抛出一个ThreadDeath异常。考虑到这一点,所有同步的方法和块都必须详细研究。
  2. 一个线程可以抛出第二个ThreadDeath异常,同时从第一个线程清除(在catch或finally子句中)。清理将不得不重复,直到它成功。确保这一点的代码将非常复杂。

所以捕获ThreadDeath异常是不可取的。