Java多线程(三)——中断线程

Posted by Lain on 10-18,2019

0

今天我们继续来看多线程。

中断线程概述

要想中断一个正在执行中的线程,在远古时期Thread提供了一个方法,stop(),但他由于会造成很多问题,现在已经不推荐使用了。我初略看了一下stop的源码注释,大意讲的是stop()方法用于强制结束线程,但他天生不安全,如果一个线程使用stop方法结束,会导致其将其锁定的所有监视器被释放(作为沿堆栈向上传播的未检查 ThreadDeath 异常的一个自然后果),进而无法保证这些监视器所保护的对象都处于一致的状态,会导致数据混乱。而且强制停止线程,也无法对已经进行的操作进行清理和回滚。

注释里也推荐了对于中断线程的一个合适的解决方法:使用stop来中断线程的代码应当被替换为使用一些变量来表示线程是否应该被停止,当前线程应当规律性的检查这些变量,当这些变量被表示为停止状态时,线程通过从run方法中返回的方式来结束线程。

从Java1.6开始JDK提供了interrupt系列方法,用于向线程发起中断通知,主要有三个方法:interrupt()、interrupted()和isInterrupted()。

interrupt() 用于向线程发出一个中断信号,这是一个对象方法,通过线程对象进行调用。值得一提的是,这个方法被调用后,除非线程正在处于阻塞状态,否者并不会影响线程的正常运行。当线程处于阻塞状态时,如:wait(),sleep(),join()等,如果线程收到了中断信号,这些方法会结束阻塞,并立刻抛出InterruptedException,然后清空线程的中断状态。这也是为什么上面那三个阻塞方法都会要求我们捕获InterruptedException ,就是为了在收到中断信号后,给我们留出处理后事的空间(大雾)。如果我们在捕获到这个异常后,不通过返回的方式结束run方法,那么线程依旧会继续跑下去,因为在捕获一次之后,线程的中断状态就被重置了。

 Tips:实际上对于线程中断状态的重置和InterruptedException的抛出,都是来自于native方法sleap和wait,join实际上也是调用的继承自Object的wait方法。

interrupted() 是一个静态方法,用于判断当前线程是否被中断,并且重置中断状态。
isInterrupted() 是一个类方法,作用和上面的一致,但区别在于它不会重置线程的中断状态。
我们可以看一下它们的源代码:

    /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */
    public boolean isInterrupted() {
        return isInterrupted(false);
    }

    /**
     * Tests if some Thread has been interrupted.  The interrupted state
     * is reset or not based on the value of ClearInterrupted that is
     * passed.
     */
    private native boolean isInterrupted(boolean ClearInterrupted);

可以看到两者都是调用的同一个native方法,且静态方法interrupted调用的是当前线程的该方法,两者区别在于传的参数不同,我们可以看最后的native方法的注释表述,可以得知是否清除线程状态取决于传入的参数。

优雅中断线程实践

变量控制法

首先我们根据JDK源码注释中的建议,通过变量来控制对线程的中断。

public class ThreadStopTest {

    private static class Worker extends Thread{
        private volatile boolean flag = false;
        @Override
        public void run() {
            while (flag){
                //doSomething
            }
	    //continue do something
        }
        public void shutdown(){
            this.flag = false;
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        worker.shutdown();
    }
}

执行结果:5s后程序退出。
这种方式比较简单明了,比较适用来中断一些用来执行类似于轮询操作的线程。当循环结束后,程序还可以进行后续的一些收尾工作,然后优雅的结束run方法。

interrupt中断法

我们通过interrupt的方式来通知线程中断。

public class ThreadStopTest2 {
    private static class Worker extends Thread{
        @Override
        public void run() {
            while (!Thread.interrupted()){
                //doSomething
            }
            System.out.println(Thread.interrupted());
            System.out.println("End Of Worker Thread");
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        worker.interrupt();
    }
}

5s后输出:

false
End Of Worker Thread

可以看到当我们结束循环后,Thread.interrupted() 依旧返回了false,证实了它确实会重置中断状态。我们还可以拿this.isInterrupted()实验,跳出循环后将会返回true。这里就不再演示这一情况了。

我们还可以利用sleep,wait,join等,在收到中断信号后会抛出异常的特性:

public class ThreadStopTest3 {
    private static class Worker extends Thread{
        @Override
        public void run() {
            while (true){
                //doSomething
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    //do something
                    break;
                }
            }
            System.out.println(Thread.interrupted());
            System.out.println("End Of Worker Thread");
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        worker.interrupt();
    }
}

5s后输出:

false
End Of Worker Thread

这里我们利用catch异常来跳出循环,可以看到,在抛出中断异常的时候,也会重置线程的中断状态。

那么问题来了。并不是所有要用到多线程的场景都是执行轮询操作,或者当我们想要结束线程的时候,并没有办法对变量或是中断状态进行判断。比如我们在while循环里,做了一个加载资源的操作,这个资源相当大,加载完要半个小时,于是这半个小时里,程序一直被阻塞在加载的操作里,即使中途我们对它发出了中断的指令,它依旧是没办法被中断的,因为根本执行不到判断中断状态的代码里面。这种情况下要如何关闭呢?

守护线程法

我们知道,当虚拟机中没有用户线程存在的时候,守护线程会立刻结束,我们可以利用这点立刻结束我们想结束的进程。

public class ThreadStopService {

    private Thread excuteThread;

    public ThreadStopService(Runnable runnable){
        this.excuteThread = new Thread(){
            @Override
            public void run() {
                Thread subThread = new Thread(runnable);
                subThread.setDaemon(true);
                subThread.start();
                try {
                    subThread.join();
                } catch (InterruptedException e) {
                    System.out.println("线程被中断!");
                }
            }
        };
    }

    public void excute(){
        this.excuteThread.start();
    }

    public void shutDown(){
        this.excuteThread.interrupt();
    }

    public static void main(String[] args) {
        ThreadStopService service = new ThreadStopService(()->{
            System.out.println("Sub Thread is Running!");
            while (true){
                //a very heavy action
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("俺还活着");
            }
        });
        service.excute();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutDown();

    }

}

输出结果:

Sub Thread is Running!
俺还活着
俺还活着
俺还活着
俺还活着
俺还活着
线程被中断!(此时整个程序都退出了)

在学习这个方法的时候我就有一个问题一直在脑海里,这种方法其实也是有极限的,那就是守护线程的结束除了自然退出,还意味着其他用户线程全部结束,如果在我们决定中断守护线程的时候,JVM里还有其他的用户线程没有结束,这时守护线程应该是没办法立刻结束的。
于是我又开了一个不会结束的用户线程:

public class ThreadStopService {

    private Thread excuteThread;

    public ThreadStopService(Runnable runnable){
        this.excuteThread = new Thread(){
            @Override
            public void run() {
                Thread subThread = new Thread(runnable);
                subThread.setDaemon(true);
                subThread.start();
                try {
                    subThread.join();
                } catch (InterruptedException e) {
                    System.out.println("线程被中断!");
                }
            }
        };
    }

    public void excute(){
        this.excuteThread.start();
    }

    public void shutDown(){
        this.excuteThread.interrupt();
    }

    public static void main(String[] args) {
	//永不结束的用户线程
        Thread main = new Thread(()->{
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("用户线程悠然自得");
            }
        });
        main.start();
        ThreadStopService service = new ThreadStopService(()->{
            System.out.println("Sub Thread is Running!");
            while (true){
                //a very heavy action
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("俺还活着");
            }
        });
        service.excute();
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutDown();

    }

}

输出结果:

Sub Thread is Running!
用户线程悠然自得
俺还活着
用户线程悠然自得
俺还活着
用户线程悠然自得
俺还活着
用户线程悠然自得
俺还活着
用户线程悠然自得
俺还活着
线程被中断!
用户线程悠然自得
俺还活着
用户线程悠然自得
俺还活着
....

可以看到,即使我们发出了结束指令,守护线程依旧没有停下,这里就涉及到我们上一篇文章的内容,线程是不存在实际意义上的父子关系的,并不能将某个线程设置为某个线程的守护线程,守护线程的终结,除了自然结束以外,只有在JVM中不存在任何非守护线程的情况下会被强制关闭,这时用户的线程也关闭了,讲课的老师看样子也没有理解这一点啊=。=