本篇主要来学习下Java中对线程中断机制的实现。在我们的程序中经常会有一些不达到目的不会退出的线程,例如:我们有一个下载程序线程,该线程在没有下载成功之前是不会退出的,若此时用户觉得下载速度慢,不想下载了,这时就需要用到我们的线程中断机制了,告诉线程,你不要继续执行了,准备好退出吧。当然,线程在不同的状态下遇到中断会产生不同的响应,有点会抛出异常,有的则没有变化,有的则会结束线程。本篇将从以下两个方面来介绍Java中对线程中断机制的具体实现:

  • Java中对线程中断所提供的API支持
  • 线程在不同状态下对于中断所产生的反应

一、Java中对线程中断所提供的API支持

在以前的jdk版本中,我们使用stop方法中断线程,但是现在的jdk版本中已经不再推荐使用该方法了。

Java线程的终止操作最初是直接暴露给用户的,java.lang.Thread类提供了stop()方法,允许用户暴力的终止一个线程并退出临界区(释放所有锁,并在当前调用栈抛出ThreadDeath Exception)。 同样的,Thread.suspend()和Thread.resume()方法允许用户灵活的暂停和恢复线程。然而这些看似简便的API在JDK1.2就被deprecate掉了,原因是stop()方法本质上是不安全的,它会强制释放掉线程持有的锁,这样临界区的数据中间状态就会遗留出来,从而造成不可预知的后果。

而由以下三个方法完成对线程中断的支持

public boolean isInterrupted()

public void interrupt()

public static boolean interrupted() 

isInterrupted()

每个线程都一个状态位用于标识当前线程对象是否是中断状态。isInterrupted是一个实例方法,主要用于判断当前线程对象的中断标志位是否被标记了,如果被标记了则返回true表示当前已经被中断,否则返回false。我们也可以看看它的实现源码:

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

private native boolean isInterrupted(boolean ClearInterrupted);

底层调用的本地方法isInterrupted,传入一个boolean类型的参数,用于指定调用该方法之后是否需要清除该线程对象的中断标识位。从这里我们也可以看出来,调用isInterrupted并不会清除线程对象的中断标识位。

interrupt()是一个实例方法,该方法用于设置当前线程对象的中断标识位。

interrupted()

interrupted()是一个静态的方法,用于返回当前线程是否被中断。

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

private native boolean isInterrupted(boolean ClearInterrupted);

该方法用于判断当前线程是否被中断,并且该方法调用结束的时候会清除中断标识位。如果一个线程被中断了,第一次调

用 interrupted 则返回 true,第二次和后面的就返回 false 了。

interrupt()

Thread.interrupt的作用其实不是中断线程,而是通知线程应该中断了,给这个线程发一个信号,告诉它,它应该结束了。至于到底是中断还是运行,应该由被通知的线程自己处理。下面让我们来看下interrupt()的源码,看下线程调用interrupt()后会发生什么

    /**
     * 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>
     *
     * <p> Interrupting a thread that is not alive need not have any effect.
     *
     * @throws  SecurityException
     *          if the current thread cannot modify this thread
     *
     * @revised 6.0
     * @spec JSR-51
     */
    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();
    }
  • 校验权限:如果不是当前线程自我中断,会先做一次权限检查。如果被中断的线程属于系统线程组(即JVM线程),checkAccess()方法会使用系统的System.getSecurityManager()来判断权限。由于Java默认没有开启安全策略,此方法其实会跳过安全检查。
  • 如果当前线程处于blocked阻塞(因为调用wait、sleep和join造成的。注意区分这里的阻塞和Thread.State枚举类的BLOCKED,夏下同)状态时被interrupt了,那么[中断标志位]将被清除(即置为false),并且收到一个InterruptedException异常。
  • 如果当前线程处于blocked阻塞(因为NIO的InterruptibleChannel进行的I/O操作造成)状态时被interrupt了,则会关闭channel[中断标志位]将会被置为true,并且当前线程会收到一个ClosedByInterruptException异常。
  • 如果当前线程处于blocked阻塞(因为NIO的Selector造成的)状态时被interrupt了,那么[中断标志位]将被置为true,然后当前线程会立即从选择器区域返回并返回值(可能为非零的值)。
  • 如果前面的情况都没有发生,则线程仅会将[中断标志位]将被置为true。

二、线程在不同状态下对于中断所产生的反应

线程一共6种状态,分别是NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED(Thread类中有一个State枚举类型列举了线程的所有状态)。下面我们就将把线程分别置于上述的不同种状态,然后看看我们的中断操作对它们的影响。

2.1 NEW和TERMINATED

线程的new状态表示还未调用start方法,还未真正启动。线程的terminated状态表示线程已经运行终止。这两个状态下调用中断方法来中断线程的时候,Java认为毫无意义,所以并不会设置线程的中断标识位,什么事也不会发生。例如:

public static void main(String[] args) throws InterruptedException {

    Thread thread = new MyThread();
    System.out.println(thread.getState());

    thread.interrupt();

    System.out.println(thread.isInterrupted());
}

输出结果如下:

NEW
false

terminated状态

public static void main(String[] args) throws InterruptedException {

     Thread thread = new MyThread();
     thread.start();

     thread.join();
     System.out.println(thread.getState());

     thread.interrupt();

    System.out.println(thread.isInterrupted());
}

输出结果如下:

TERMINATED
false

从上述的两个例子来看,对于处于new和terminated状态的线程对于中断是屏蔽的,也就是说中断操作对这两种状态下的线程是无效的。

2.2 Runnable

处于RUNNABLE状态的线程在遭遇中断操作的时候只会设置该线程的中断标志位,并不会让线程实际中断,想要发现本线程已经被要求中断了则需要用程序去判断。例如:

public class MyThread extends Thread {
    @Override
    public void run() {
        while (true){
            //do something
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new MyThread();
        thread.start();

        System.out.println(thread.getState());

        thread.interrupt();
        Thread.sleep(1000);
        System.out.println(thread.isInterrupted());

        System.out.println(thread.getState());
    }
}

运行结果

RUNNABLE
true
RUNNABLE

可以看到在我们启动线程之后,线程状态变为RUNNABLE,中断之后输出中断标志,显然中断位已经被标记,但是当我们再次输出线程状态的时候发现,线程仍然处于RUNNABLE状态。很显然,处于RUNNBALE状态下的线程即便遇到中断操作,也只会设置中断标志位并不会实际中断线程运行。那么问题是,既然不能直接中断线程,我要中断标志有何用处?

这里其实Java将这种权力交给了我们的程序,Java给我们提供了一个中断标志位,我们的程序可以通过if判断中断标志位是否被设置来中断我们的程序而不是系统强制的中断。例如:

/*修改MyThread类的run方法*/
public void run(){
    while(true){
        if (Thread.currentThread().isInterrupted()){
            System.out.println("exit MyThread");
            break;
        }
    }
}

线程一旦发现自己的中断标志为被设置了,立马跳出死循环。这样的设计好处就在于给了我们程序更大的灵活性。

3、BLOCKED
当线程处于BLOCKED状态说明该线程由于竞争某个对象的锁失败而被挂在了该对象的阻塞队列上了。那么此时发起中断操作不会对该线程产生任何影响,依然只是设置中断标志位。例如:

public class MyThread extends Thread {
    @Override
    public void run() {
        doSomething();
    }
    public synchronized static void doSomething(){
        while(true){
            //do something
        }
    }
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new MyThread();
        thread1.start();

        Thread thread2 = new MyThread();
        thread2.start();

        Thread.sleep(1000);
        System.out.println(thread1.getState());
        System.out.println(thread2.getState());

        thread2.interrupt();
        System.out.println(thread2.isInterrupted());
        System.out.println(thread2.getState());
    }
}

运行结果:

RUNNABLE
BLOCKED
true
BLOCKED

在我们的主线程中,我们定义了两个线程并按照定义顺序启动他们,显然thread1启动后便占用MyThread类锁,此后thread2在获取锁的时候一定失败,自然被阻塞在阻塞队列上,而我们对thread2进行中断。从输出结果看来,thread2处于BLOCKED状态,执行中断操作之后,该线程仍然处于BLOCKED状态,但是中断标志位却已被修改。这种状态下的线程和处于RUNNABLE状态下的线程是类似的,给了我们程序更大的灵活性去判断和处理中断。

2.4 WAITTING 和 TIMED_WAITING

这两种状态本质上是同一种状态,只不过TIMED_WAITING在等待一段时间后会自动释放自己,而WAITING则是无限期等待,需要其他线程调用notify方法释放自己。但是他们都是线程在运行的过程中由于缺少某些条件而被挂起在某个对象的等待队列上。当这些线程遇到中断操作的时候,会抛出一个InterruptedException异常,并清空中断标志位。例如:

public class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (this){
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("I am waiting but facing InterruptException now");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new MyThread();
        thread.start();

        Thread.sleep(500);
        System.out.println(thread.getState());
        thread.interrupt();
        Thread.sleep(1000);
        System.out.println(thread.isInterrupted());
    }
}

在main线程中我们启动一个MyThread线程,然后对其进行中断操作。

从运行结果看,当前程thread启动之后就被挂起到该线程对象的条件队列上,然后我们调用interrupt方法对该线程进行中断,输出了我们在catch中的输出语句,显然是捕获了InterruptedException异常,接着就看到该线程的中断标志位被清空。

综上所述,我们分别介绍了不同种线程的不同状态下对于中断请求的反应。NEW和TERMINATED对于中断操作几乎是屏蔽的,RUNNABLE和BLOCKED类似,对于中断操作只是设置中断标志位并没有强制终止线程,对于线程的终止权利依然在程序手中。WAITING/TIMED_WAITING状态下的线程对于中断操作是敏感的,他们会抛出异常并清空中断标志位。

参考资料

https://blog.csdn.net/canot/article/details/51087772

https://www.jianshu.com/p/e2b22c6bcd22

https://www.cnblogs.com/yangming1996/p/7612653.html