理解中断

中断是线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程发送了消息,其他线程通过调用该线程的 interrupt() 方法对其进行中断操作,在该线程内部可以使用 thread.isInterrupted() 进行优雅的终止线程。

线程的几种状态 中提到线程有两种方式来响应中断操作:

  • 方式一:

在调用线程对象的 interrupt() 后,线程如果处于 WAITING 或 TIMED_WAITING 时(包括阻塞于等待 IO 操作的 RUNNABLE 状态),会触发 InterrutedExecption 异常(此时,JVM 会先将该线程的中断标识位清除,然后抛出 InterruptedException,针对于所有方法签名上有该异常的方法),但是当触发 InterrutedExecption 异常后中断标记会被清空,这时需要在重置中断标记(如:Thread.currentThread().interrupt())。

  • 方式二:

如果线程处于 RUNNABLE 状态,没有阻塞在某个 IO 操作上或没有处于等待某个事件完成,则线程对象需要通过主动轮询来判断自己是否被中断了,可以调用 isInterrupted() 方法,检测自己是不是已经被中断。

另外,如果对应已经处于终止状态(TERMINATED),那么即使该线程被中断过,在调用该线程对象的 isInterrupted() 时依旧会返回 false。

中断示例

首先创建了两个线程,SleepThread 和 BusyThread,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。

 public static void main(String[] args) throws Exception {
     Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
     sleepThread.setDaemon(true);
     Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
     busyThread.setDaemon(true);
     sleepThread.start();
     busyThread.start();
     // 休眠 5 秒,让 sleepThread 和 busyThread 充分运行
     TimeUnit.SECONDS.sleep(5);
     sleepThread.interrupt();
     busyThread.interrupt();
     System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
     System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
     // 防止 sleepThread 和 busyThread 立刻退出
     TimeUnit.SECONDS.sleep(5);
 }

static class SleepRunner implements Runnable {
    @Override
    public void run(){
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

static class BusyRunner implements Runnable {
    @Override
    public void run() {
        while (true) {
        }
    }
}
SleepThread interrupted is false
BusyThread interrupted is true

从运行结果看出,抛出 InterruptedException 的线程 SleepThread,其中断标识位被清除了,而一直忙碌运作的线程 BusyThread,中断标识位没有被清除。

Future.cancel() 与 interrupt()

Future 类有一个 cancel 方法,该方法有个 mayInterruptIfRunning 的 boolean 类型参数。

boolean cancel(boolean mayInterruptIfRunning);

mayInterruptIfRunning 参数表示任务是否能被中断,如果为 true,表示当前正在某个线程运行的任务可以被中断;false 表示任务可以完成或者被取消但不处理中断请求。

再来看看 Future 接口的一个实现:

public class FutureTask<V> implements RunnableFuture<V> {

    ...

    public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

    ...
}

从 FutureTask 的实现看出,如果 mayInterruptIfRunning 为 false,则不中断当前运行任务的线程,等待运行结束,并执行 finishCompletion 唤醒那些等待某个事件发生的线程。如果 mayInterruptIfRunning 为 true,则将当前正在执行计算任务的线程中的中断,再唤醒等待线程。

通过 cancel 方法的第一个 if 判断可以参数,如果任务已经运行结束,则不会产生任何影响。

下面程序通过 Future 来取消任务

public static void timedRum(Runnable r, long timeout, TimeUnit unit) throws InterruptedException{
    ExecutorService taskExec = Executors.newSingleThreadExecutor();
    Future<?> task = taskExec.submit(r);
    try {
        task.get(timeout, unit);
    } catch (ExecutionException e) {
        // 此处可以重新封装异常再抛出
    } catch (TimeoutException e) {
        // 接下來的任务将被取消
    }finally {
        // 对于结束任务,不会有什么影响
        // 对于正在执行的任务,正在执行任务的线程将被中断然后执行取消操作
        task.cancel(true);
    }
}

当 Future.get 抛出 InterruptedException 异常或 TimeoutException 异常时,如果不再需要结果,可以使用 Future.cancel 来取消任务。

虽然 Future.cancel() 与 interrupt() 都可以中断线程,但 Future.cancel() 提供了更加完善的机制,可以选择不中断,让运行计算任务的线程自行结束,也可以让线程中断,并做一些收尾处理,如:依赖 AQS 唤醒阻塞队列的线程。

synchronized 的中断问题

在 JVM 自带的锁机制 synchronized 中,当一个线程获取不到锁而被阻塞在 synchronized 之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在 synchronized 上,等待着获取锁。线程有两种方式来响应中断操作,而 synchronized 没有类似在可中断方法上方法签名有 InterruptedException 的标识,并且在 BLOCKED 状态下也无法通过轮询来获取当前线程是否被中断的信息,所以在 synchronized 是无法响应中断的。

参考

最后修改日期: 2019年12月17日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。