浅析线程的正确停止

5,192 阅读12分钟

如何正确停止线程

1. 讲解原理

原理介绍:使用interrupt来通知,而不是强制。 Java中停止线程的原则是什么?

在Java中,最好的停止线程的方式是使用中断interrupt, 但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。

任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自行停止。然而,有时候我们希望提前结束任务或线程或许是因为用户取消了操作,或者服务需要被快速关闭,或者是运行超时或出错了。

要使任务和线程能安全、快速、可靠地停止下来并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式当需要停止时,它们首先会清除当前正在执行的工作然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作

生命周期结束(End- of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运的软件之间的最主要区别就是行为良好的软件能很完善地处理失败、关闭和取消等过程。

本章将给出各种实现取消和中断的机制,以及如何编写任务和服务,使它们能对取消请求做出响应

2. 最佳实践:如何正确停止线程

2.1 线程通常会在什么情况下停止

1、run()方法运行完毕
2、 有异常出现,并且线程中没有捕获。
线程停止以后,所占用的资源会被jvm回收。

2.2 正确的停止方法:interrupt

2.2.1 通常情况下如何停止

package stopthreads;

/**
 * 描述: run()方法内没有sleep()和wait()方法时,停止线程。
 */

public class RightStopThreadWithoutSleep implements Runnable{

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

    }
    @Override
    public void run() {
        int num = 0;
        while (num <= Integer.MAX_VALUE / 2){
            if (!Thread.currentThread().isInterrupted() && num % 10000 == 0) {
                System.out.println(num + "是10000的倍数");
            }
            num++;
        }
        System.out.println("任务结束了。");
    }
}

注意: thread.interrupt();无法强制的中断线程,必须有要被中断的线程的配合。
即:需要在子线程中加上如下代码:!Thread.currentThread().isInterrupted()

2.2.2 线程可能被阻塞如何停止

package stopthreads;

import static java.lang.Thread.*;

public class RightStopThreadWithSleep {
    
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            while (num <= 300){
                if (num % 100 == 0 && !currentThread().isInterrupted()){
                    System.out.println(num + "是100的整数倍");
                }
                num++;
            }
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }

}

结果如下:

2.2.3 如果线程在每次迭代后都阻塞

package stopthreads;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

/**
 * 描述:如果在执行过程中,每次循环都会调用sleep()或wait()等方法,那么...
 */
public class rightStopThreadWithSleepEveryLoop {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            try {
                while (num <= 10000){
                    if (num % 100 == 0 && !currentThread().isInterrupted()){
                        System.out.println(num + "是100的整数倍");
                    }
                    num++;
                    sleep(10);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

}

while内的try-catch问题:

package stopthreads;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;

                while (num <= 10000){
                    if (num % 100 == 0 && !currentThread().isInterrupted()){
                        System.out.println(num + "是100的整数倍");
                    }
                    num++;
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

}

改变try-catch位置,结果完全不一样:

注意: 即便添加上:&& !currentThread().isInterrupted()依然没有效果!
原因: Thread类在设计时,sleep()调用中断后,interrupt标记物会自动清除!

2.3 实际开发中的两种最佳实践

2.3.1 最佳实践一:优先选择:传递中断(方法的签名上抛出异常)

我们先加一个小插曲: 错误地处理异常, 在被调用的方法中,直接把InterruptException catch掉,这样做相当于在低层的方法中就把异常给吞了,导致上层的调用无法感知到有异常。
正确做法应该是:抛出异常, 而异常的真正处理,应该叫个调用它的那个函数。
错误代码如下:

package stopthreads;

/**
 * 描述:  catch了InterruptionException之后的优先选择:在方法签名中抛出异常。
 * 那么,在run()中就会强制try-catch。
 */
public class RightWayStopThreadInProduction implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction());
        thread.start();
        thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while(true){
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() {

        /**
         * 错误做法:这样做相当于就把异常给吞了,导致上层的调用无法感知到有异常
         * 正确做法应该是,抛出异常,而异常的真正处理,应该叫个调用它的那个函数。
         */
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

错误处理异常导致线程无法停止:


正确做法:抛出异常,而异常的真正处理,应该交给调用它的那个函数。
低层方法,抛出异常,调用者,就只有Surround with try/catch了。

正确代码如下:

package stopthreads;

import static java.lang.Thread.sleep;

/**
 * 描述:  catch了InterruptionException之后的优先选择:在方法签名中抛出异常。
 * 那么,在run()中就会强制try-catch。
 */
public class RightWayStopThreadInProduction implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction());
        thread.start();
        sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while(true){
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() throws InterruptedException {

        /**
         * 错误做法:这样做相当于就把异常给吞了,导致上层的调用无法感知到有异常
         * 正确做法应该是,抛出异常,而异常的真正处理,应该叫个调用它的那个函数。
         */
       /* try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

       sleep(2000);

    }
}

总结:

2.3.2 最佳实践二:不想或无法传递:恢复中断(再次人为的手动把中断恢复)

在低层方法中可以try-catch,但是一定要添加 Thread.currentThread().interrupt();

package stopthreads;

import static java.lang.Thread.sleep;

public class RightWayStopThreadInProduction2 implements Runnable{

    @Override
    public void run() {

        while(true){
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("线程中断");
                break;
            }
            reInterrupt();
        }

    }

    private void reInterrupt() {

        try {
            sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction2());
        thread.start();
        sleep(1000);
        thread.interrupt();
    }
}

结果:

总结:如果不能抛出中断,要怎么做?

如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException), 那么应该选择在catch子句中调用Thread.currentThread() interrupt()来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。具体代码见上,在这里,线程在sleep期间被中断,并且由catch捕获到该中断,并重新设置了中断状态,以便于可以在下一个循环的时候检测到中断状态,正常退出。

不应屏蔽中断

2.4 正确停止带来的好处

3. 停止线程的错误方法

3.1 错误停止一:被弃用的stop,suspend,resume方法

用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(代码中是一个连队),会造成脏数据(有的连队多领取少领取装备)。

package threadcoreknowledge.createthreads.stopthreads;

/**
 * 描述:     错误的停止方法:用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)。
 */
public class StopThread implements Runnable{

    @Override
    public void run() {
        //模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
        for (int i = 0; i < 5; i++) {
            System.out.println("连队" + i + "开始领取武器");
            for (int j = 0; j < 10; j++) {
                System.out.println(j);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("连队"+i+"已经领取完毕");
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new StopThread());
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.stop();
    }


}

还有一种错误的理论: 即使用stop()不会释放,monitor(监视器)锁,会造成程序的卡死。 官方有明确说明,stop(),会释放的monitor(监视器)。

3.2 错误停止二:用volatile设置boolean标记位

看似可行

package threadcoreknowledge.stopthreads.volatiledemo;

/**
 * 描述:     演示用volatile的局限:part1 看似可行
 */
public class WrongWayVolatile implements Runnable {

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数。");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;
    }
}

如何不行

storage.put(num);处被阻塞了,无法进入新的一层while()循环中判断,!Canceled 的值也就无法判断

代码演示:java

package threadcoreknowledge.createthreads.wrongway.volatiledemo;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        Producer producer = new Producer(storage);
        Thread thread = new Thread(producer);
        thread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(storage.take()+"被消费");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了");
        /**
         *  一旦消费不需要更多数据了,我们应该让生产者也停下来,
         *  但是实际情况,在 storage.put(num);处被阻塞了,无法进入新的一层while()循环中判断,!Canceled 的值也就无法判断
         */
        producer.canceled = true;
        System.out.println(producer.canceled);

    }
}
class Producer implements Runnable{

    public volatile boolean canceled = false;
    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            //canceled为true,则无法进入
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }

}

class Consumer {

    BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}

结果:程序并没有结束。

程序并没有结束

进行修复

使用interrupt: 代码演示:

package threadcoreknowledge.createthreads.wrongway.volatiledemo;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class WrongWayVolatileFixed  {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(storage.take()+"被消费");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了");
        /**
         *  一旦消费不需要更多数据了,我们应该让生产者也停下来,
         *  但是实际情况,在 storage.put(num);处被阻塞了,无法进入新的一层while()循环中判断,!Canceled 的值也就无法判断
         */
        producerThread.interrupt();

    }
    class Producer implements Runnable{

        BlockingQueue storage;

        public Producer(BlockingQueue storage) {
            this.storage = storage;
        }

        @Override
        public void run() {
            int num = 0;
            try {
                //canceled为true,则无法进入
                while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍数,被放到仓库中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者结束运行");
            }
        }

    }

    class Consumer {

        BlockingQueue storage;

        public Consumer(BlockingQueue storage) {
            this.storage = storage;
        }

        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}

结果:程序正常结束。

程序正常结束

总结:

总结

4. 停止线程重要函数源码解析

4.1 interrupt()源码分析

interrupt()源码分析

4.2 判断中断的相关方法分析

判断中断的相关方法

4.2.1 static boolean interrupted()

源码如下:

  /**
     * 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);
    }

该方法会调用private native boolean isInterrupted(boolean ClearInterrupted);并传入'true'('true'代表是否清除当前的状态。)来判断线程的状态,并返回true/false(返回true代表该线程已经被中断,返回false,代表该线程在继续进行)。并且在返回之后,会把线程的中断状态直接设为false。也就是说,直接把线程中断状态直接给清除了,这也是唯一能清除线程中断状态的方法。(由于private native boolean isInterrupted(boolean ClearInterrupted);native修饰,故无法调用)

4.2.2 boolean isInterruted()

和static boolean interrupted();一样,都会返回当前线程的中断状态,但是isInterrpted()方法不会清除中断状态。

4.2.3 Thread.interrupted()作用对象

Thread.interrupted()作用对象实际是调用它的线程。与谁打点调并没有关系,不管是Thread调用,还是Thread的实例(thread)调用,interrupted()方法并不关系,它只关心的是时他自己处于哪个线程,在哪个线程,就返回该线程的中断状态,并返回后进行清除。

小练习:想一想,下面程序会输出什么样的结果。

package threadcoreknowledge.createthreads.stopthreads;

public class RightWayInterrupted {

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

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
                }
            }
        });
        // 启动线程
        threadOne.start();
        //设置中断标志
        threadOne.interrupt();
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());  //true
        //获取中断标志并重置
        System.out.println("isInterrupted: " + threadOne.interrupted()); // 会清除中断标志位 false
        //获取中断标志并重直
        System.out.println("isInterrupted: " + Thread.interrupted());  //两次清除 true
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted()); //ture
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}

运行结果如下:

运行结果

5. 常见面试问题

5.1 如何正确停止一个线程?

可以从以下三个方面进行阐释:

  1. 原理:用 interrupt 来请求线程停止而不是强制,好处是安全。
  2. 三方配合:想停止线程,要请求方、被停止方、子方法被调用方相互配合才行:
    a. 作为被停止方:每次循环中或者适时检查中断信号,并且在可能抛出 InterrupedException 的地方处理该中断信号;
    b. 请求方:发出中断信号;
    c. 被调用的优先在方法层面抛出,而不是捕获异常 InterrupedException,如果不能抛出异常,也可以在try-catch中再次设置中断状态;
  3. 最后再说错误的方法:stop/suspend 已废弃,volatile 的 boolean 无法处 理长时间阻塞的情况

5.2 如何处理不可中断的阻塞(例如抢锁时 ReentrantLock.lock()或者 Socket I/O 时无法响应中断,那应该怎么让该线程停止呢?)

如果线程阻塞是由于调用了 wait(),sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来唤醒该线程。 但是对于不能响应 InterruptedException 的阻塞,很遗憾,并没有一个通用的 解决方案。 但是我们可以利用特定的其它的可以响应中断的方法,比如:ReentrantLock.lockInterruptibly(),比如:关闭套接字使线程立即返回等方法 来达到目的。