前面介绍过了几种线程状态和几种状态之间的转换白话java锁–线程状态。此篇文章主要介绍的是对线程中断的理解。其实我一直不太理解为什么中断的时候线程会抛出个InterruptedException异常。

线程中断API

在以前的版本中使用stop()方法中断线程,但是该方法已经废弃了

@Deprecated
public final void stop() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        checkAccess();
        if (this != Thread.currentThread()) {
            security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
    // A zero status value corresponds to "NEW", it can't change to
    // not-NEW because we hold the lock.
    if (threadStatus != 0) {
        resume(); // Wake up thread if it was suspended; no-op otherwise
    }

    // The VM can handle all thread states
    stop0(new ThreadDeath());
}

现在的线程终止使用Thread类中的如下方法:

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}


public boolean isInterrupted() {
    return isInterrupted(false);
}

总共分为三个方法,主要就是interrupt()方法,下面详细讲解一下这个方法

interrupt()

一步一步看interrupt()方法的实现

if (this != Thread.currentThread())
	checkAccess();

首先判断调用interrupt()方法的线程是否是当前线程(一个线程当然可以中断自己),如果另一个线程想要中断当前线程,需要调用checkAccess()方法进行权限校验。

public final void checkAccess() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkAccess(this);
    }
}

checkAccess()方法里面的实现就是根据是否启用了SecurityManager来判断线程权限(启动的方式就是在jvm启动参数加上-Djava.security.manager,根据配置文件的配置校验权限)

interrupt0();

先看这个方法,这个方法的实现是:

private native void interrupt0();

是一个native方法,是通过JNI调用的c语言的方法,但是通过注释

// Just to set the interrupt flag

我们可以知道,调用这个方法只是设置了interrupt的标识。那么什么是interrupt标识呢?我们再读一段注释:

* <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}.

这段注释的主要意思是说:如果线程被阻塞在了

Object的

  • wait()
  • wait(long)
  • wait(long,int)

Thread的

  • join()
  • join(long)
  • join(long,int)
  • sleep(long)
  • sleep(long,int)
    然后他的interrupt状态会被清除,并且会得到一个InterruptedException异常(这里终于提到这个异常了)。

对于上面的话可能会懵,首先解释一下什么叫interrupt状态(interrupt标识)。通过一个简单的例子就可以很好理解:

private volatile boolean cancelled;

public void run() {
    //如果取消,则退出
    while (!cancelled) {
        //do something
    }
}

public void cancel() {
    cancelled = true;
}

给执行run()方法中设置一个标识,然后再调用cancel()方法将标识位设置为取消,那么run()中的方法就自然退出了。通过这个例子就可以了解到其实interrupt标识就是上面例子中的while判断标识。

实际上在执行interrupt0()方法的时候底层的原理就是和上面的例子一样,线程在调用interrupt0()方法的时候将interrupt标识设置为true,线程会不断的判断这个interrupt标识,来决定线程是否应该被中断。所以这就是和stop()方法的不同,stop()方法是不管代码执行到哪里都会直接将线程停止,但是使用interrupt标识的话,就可以由程序代码决定线程终止之后的行为。

那么在了解了interrupt标识之后,我们再解释一下为什么在调用interrupt()方法的时候,如果线程因为调用这些sleep()、join()、wait()方法而阻塞的时候,interrupt状态会被清除,然后抛出InterruptedException异常。通过上一节线程状态可以知道,当调用完这些方法之后,线程会处于等待状态(等待其他线程调用notify()方法或者notifyAll()方法),而这个线程自己则会因为等待某个条件而一动不动,所以要想让这种线程停止(并且不使用唤醒方法),在java中的办法就是让这个阻塞线程自己抛出异常,从而使线程退出阻塞状态,通过try/catch捕获到异常,知道这个线程因为阻塞而中断了,后续可以定制异常处理。

再看interrupt()方法的下面一段

synchronized (blockerLock) {
    Interruptible b = blocker;
    if (b != null) {
        interrupt0();           // Just to set the interrupt flag
        b.interrupt(this);
        return;
    }
}

此段代码主要针对的是IO/NIO的操作

* <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.

通过注释可以了解到:

  • 如果一个线程阻塞在了I/O操作上,并且这个I/O是实现了InterruptibleChannel接口的,如果这个通道要关闭,线程的中断状态会被设置,并且会收到ClosedByInterruptException异常
  • 如果一个线程阻塞在了Selector上,会设置这个线程的中断状态并且立即从selection操作中返回,可能是个空值,就像调用了wakeup()方法一样。如果此时再次调用执行wakeup()方法则会抛出ClosedSelectorException异常

其实上面的两段话和抛出InterruptedException异常的想法有点类似,都是如果因为IO问题而阻塞的时候,如果需要关闭这个线程,这个线程会抛出异常,代码里面处理后续关闭异常逻辑即可。

总结

稍微总结一下java对于中断的处理,其实就分为两步

第一步就是给所有线程一个标志位,然后这个线程不停的去判断这个标志位是否改变,如果想要终止这个线程就改变这个标志位,然后线程知道标志位改变了,执行后续的终止逻辑

第二步就是如果这个线程因为IO、网络原因或者处于等待锁的状态时,这个线程不能继续往下进行了,如果中断这个线程,这个线程会立即从阻塞状态中退出,并抛出InterruptedException异常,告诉上层我退出了,后续再做退出后的处理

所以无论是哪一步都是给予这个线程的建议,当然也可以忽略(简单的try/catch),继续执行,好像什么都没有发生,所以在java中,中断只是传递了一个请求中断的请求,并不会直接阻止一个线程的运行。

扩展

在这里扩展一下线程每种状态下如果中断会对他们造成什么影响

  • NEW/TERMINATED : 如果此时调用中断方法是毫无意义的,因为线程还未真正启动,而TERMINATED则表示线程已经死亡,所以不会设置中断标识位,而且什么事都不会发生
  • RUNNABLE : 处于这种状态的线程不一定会获取CPU执行权限,因为在一个时间段里面,CPU只能执行一个线程,其他线程虽然是RUNNABLE状态,但是没有获得CPU执行权限。如果中断这种类型的线程只会设置这个线程的中断标志位,不会实际中断,线程会继续运行,中断应该由我们程序控制,而不是交给系统强制停止。给了程序很大的灵活性
  • BLOCKED : 这种状态和RUNNABLE状态的线程中断效果类似,这种线程只是因为某个对象锁而阻塞了,但是还是有竞争机会的,还是有可执行权的,所以这种类型的线程能够继续运行判断中断标识位的状态
  • WAITING/TIMED_WAITING : 处于这种状态的线程在中断的时候就是上面我所说的抛出InterruptedException异常的情况,在抛出异常的同时会清空中断标志位(也就是还原中断标识位为可中断状态),后续可以通过程序重新设置

综上所述,NEW/TERMINATED对于中断是没有反应的,因为也没有意义;RUNNABLE/BLOCKED对于中断只是设置的标识位,并不是强制中断,终止的权限还是在程序手中的;WAITING/TIMED_WAITING对于中断的反应是强烈的,会抛出异常并重置中断标识