Java “优雅”地中断线程(实践篇)

1,051 阅读5分钟

前言

线程并发系列文章:

Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列
在Android开发中,不可避免的会用到线程来执行耗时任务,那如果我们想在中途停止/中断任务的执行,该怎么办呢?先来看看一个简单的线程。

    private Thread threadOne = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                while(true) {
                    Log.d(TAG, "thread isAlive:" + threadOne.isAlive());
                    Thread.sleep(1000);
                }
            } catch (Exception e) {
                Log.d(TAG, e.getClass().toString());
                Log.d(TAG, "thread isAlive in catch:" + threadOne.isAlive());
            }
        }
    });

正常运行打印结果:

image.png

当使用interrupt()方法中断该线程时,打印如下:

image.png

可以看出,调用interrupt()后,会捕获名为“InterruptedException”的异常,但是接下来的发现线程还存活,这是怎么回事呢?既然线程能够被中断,那么是否提供查询中断状态的方法呢?通过查看api我们发现,thread.isInterrupted()可以查看线程的中断状态,因此我们再加一个打印:

                Log.d(TAG, e.getClass().toString());
                Log.d(TAG, "thread isInterrupted::" + threadOne.isInterrupted());
                Log.d(TAG, "thread isAlive in catch:" + threadOne.isAlive());

然而中断状态位依然是“未被中断”。这与我们想象的不太一样,因此回想一下是哪个方法抛出了异常,发现是sleep方法。

// BEGIN Android-changed: Implement sleep() methods using a shared native implementation.
    public static void sleep(long millis) throws InterruptedException {
        sleep(millis, 0);
    }

我们先把sleep()方法注释掉,再运行

    private Thread threadOne = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                while (!threadOne.isInterrupted()) {
                    Log.d(TAG, "thread isAlive:" + threadOne.isAlive());
                }
                Log.d(TAG, "break while thread isAlive:" + threadOne.isAlive());
                Log.d(TAG, "break while thread isInterrupted::" + threadOne.isInterrupted());
            } catch (Exception e) {
                Log.d(TAG, e.getClass().toString());
                Log.d(TAG, "thread isInterrupted::" + threadOne.isInterrupted());
                Log.d(TAG, "thread isAlive in catch:" + threadOne.isAlive());
            }
        }
    });

image.png

这次的结果比较符合我们的“直观想象”, 线程还是存活,但中断状态位标记位为true。

从上面两个两个例子可知:
1.中断正在阻塞(sleep)的线程,会抛出InterruptedException异常,中断标记位为false。
2.中断未被阻塞的线程,中断标记位会被置为true。

那么针对正在阻塞的线程,我们只需要捕获到InterruptedException异常就退出线程执行,对于未被阻塞的线程,判断中断标记是否为true,若是则退出线程执行。当然我们线程里如果调用了其它方法,不确定其它方法阻塞与否,因此可以将这两种判断结合起来,如下:

    private Thread threadOne = new Thread(new Runnable() {
        @Override
        public void run() {

            try {
                while (!threadOne.isInterrupted()) {
                    doSomething();
                }

            } catch (Exception e) {
                if (e instanceof InterruptedException) {
                    isInterrupted = true;
                }
            }
        }
    });

也许你会疑惑说你上面的线程都是在while里跑,如果线程只走一次,怎么中断呢?只能尽可能在每个关键之处停止其执行。

    private Thread threadOne = new Thread(new Runnable() {
        @Override
        public void run() {

            try {
                if (!threadOne.isInterrupted())
                    doSomething1();
                else
                    return;

                if (!threadOne.isInterrupted())
                    doSomething2();
                else
                    return;

                if (!threadOne.isInterrupted())
                    doSomething3();
                else
                    return;

            } catch (Exception e) {
                
            }
        }
    });

除了sleep方法之外,还有其它方法会抛出异常不?实际上,在Thread源码里对此都有解释,我们来看看源码怎么说的。

    /**
     * Interrupts this thread.
     *
     * <p> Unless the current thread is interrupting itself, which is
     * always permitted, the {@link #checkAccess() checkAccess} method
     * of this thread is invoked, which may cause a {@link
     * SecurityException} to be thrown.
     *
     * <p> If this thread is blocked in an invocation of the {@link
     * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
     * Object#wait(long, int) wait(long, int)} methods of the {@link Object}
     * class, or of the {@link #join()}, {@link #join(long)}, {@link
     * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
     * methods of this class, then its interrupt status will be cleared and it
     * will receive an {@link InterruptedException}.
     *
     * <p> If this thread is blocked in an I/O operation upon an {@link
     * java.nio.channels.InterruptibleChannel InterruptibleChannel}
     * then the channel will be closed, the thread's interrupt
     * status will be set, and the thread will receive a {@link
     * java.nio.channels.ClosedByInterruptException}.
     *
     * <p> If this thread is blocked in a {@link java.nio.channels.Selector}
     * then the thread's interrupt status will be set and it will return
     * immediately from the selection operation, possibly with a non-zero
     * value, just as if the selector's {@link
     * java.nio.channels.Selector#wakeup wakeup} method were invoked.
     *
     * <p> If none of the previous conditions hold then this thread's interrupt
     * status will be set. </p>

总结来说:

  • 在线程里使用sleep、wait、join等方法,当线程被中断时,中断状态位会被重置为false,并且抛出InterruptedException异常(这也是为什么我们第一个例子里thread.isInterrupted()为false的原因)
  • 在线程里使用nio InterruptibleChannel接口时,当线程被中断时,中断状态位会被重置为true,并且抛出ClosedByInterruptException异常
  • 在线程里使用nio Selector时,当线程被中断时,中断状态位会被重置为true
  • 如不属于上述条件,则中断状态位会被重置为true(对应我们上面说的没有阻塞的情况)

thread.isInterrupted() 和 Thread.interrupted()区别
thread.isInterrupted()是对象方法,表示thread的中断状态。Thread.interrupted()是静态方法,表示当前线程的中断状态,举个例子:

        Log.d(TAG, " curThread is:" + Thread.currentThread().getName());
        Log.d(TAG, " Thread.currentThread().isInterrupted() before :" + Thread.currentThread().isInterrupted());
        Log.d(TAG, " Thread.interrupted() before :" + Thread.interrupted());
        Log.d(TAG, " threadOne.isInterrupted() before :" + threadOne.isInterrupted());
        Thread.currentThread().interrupt();
        Log.d(TAG, " Thread.currentThread().isInterrupted() after:" + Thread.currentThread().isInterrupted());
        Log.d(TAG, " Thread.interrupted() after :" + Thread.interrupted());
        Log.d(TAG, " Thread.currentThread().isInterrupted() after2:" + Thread.currentThread().isInterrupted());
        Log.d(TAG, " threadOne.isInterrupted() after :" + threadOne.isInterrupted());

image.png

从上面可以看出来,Thread.interrupted()调用后会重置中断状态为false,而thread.isInterrupted()却不会。

总结

1、线程正在执行sleep、join、wait等方法,此时线程处在WAITING/TIMED_WAITING状态,当执行thread.interrupt(),那么会抛出InterruptedException异常,线程中断标记位为false,线程停止运行;
2、线程处在RUNNABLE状态,当执行thread.interrupt(),不会抛出异常,线程中断标记位为true,线程未停止运行;
3、如果线程处在BLOCKED(Synchronized争抢锁)状态,当执行thread.interrupt(),不会抛出异常,线程中断标记位为true,线程未停止运行(这点也说明了Synchronized不可打断)

更多关于线程状态的问题请移步:Java 线程状态

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android

更多干货,公众号搜索【小鱼人爱编程】