如何正确停止线程

486 阅读3分钟

  首先需要强调的是中断线程只有一种方法,就是Thread类的interrupt方法。
  接下来通过代码演示两种最佳中断线程的方式,以及关于中断线程需要注意的点,请仔细体会每一个示例代码。
  先来看一个最基本的中断线程的情况(子线程中无sleep方法):

public class RightWayStopThreadWithoutSleep implements Runnable {

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

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

    }
}

  执行这段代码会发现,thread.interrupt()这行代码好像并不起作用,子线程还是输出了所有的结果(我们想要的结果是子线程只能输出1秒,然后发生中断)。子线程并没有理会我们的中断请求。这里要强调的是使用interrupt来通知,而不是强制线程停止,它是否响应中断完全取决操作系统。
  因为在HotSpot虚拟机中,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,所以HotSpot自己是不会去干涉线程调度的,全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的。
  回到上面的代码,想要达到我们的目的,应该要这样写(只展示核心部分):

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

  通过Thread.currentThread().isInterrupted()判断线程是否处于中断状态,通过逻辑判断决定是否继续执行while循环。
  再来看个子线程中带有sleep方法的中断例子:

public class RightWayStopThreadWithSleep {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                while (num <= 9999999) {
                    if (num % 10 == 0) {
                        System.out.println(num + "是10的倍数");
                    }
                    num++;
                }
                //思考点
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }
}

  这里的运行结果也是和上面一样并没有理会我们的中断请求,需要解释下的是:主线程休眠了100毫秒再对子线程进行中断,此时子线程还在执行while循环,并没有执行到Thread.sleep(10)这一行代码,所以线程也没有理会我们的中断请求。所以要想达到我们的效果,应该和上面一样,在while循环的时候加上Thread.currentThread().isInterrupted()的判断。

一.最佳之传递中断

  直接看代码:

public class RightWayStopThreadInProd implements Runnable {

    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
            	//恢复中断的标记位,这里暂时不用管,下面会解释。
                Thread.currentThread().interrupt();
                //保存日志、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }

    private void throwInMethod() throws InterruptedException {
            Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        //这里代表主线程停顿1秒,让子线程能打印一会。
        Thread.sleep(1000);
        //中断子线程
        thread.interrupt();
    }
}

  如果throwInMethod方法不想处理异常,应该在方法签名中抛出异常,交给调用者去处理。因为往上抛到最顶层,也就是run()就会强制try/catch,run方法是不能抛异常的。

二.最佳之恢复中断

public class RightWayStopThreadInProd2 implements Runnable {

    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted,程序运行结束");
                break;
            }
            reInterrupt();
        }
    }

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

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

  在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断。因为sleep()方法响应中断请求后或者说抛出异常后,会清除中断标志位,所以要恢复中断。
  两种方法介绍完了,所以我们不应该在开发中不理会中断,也就是catch InterruptedException后什么也不做,这是不好的实现。

三.易出错的点

3.1 如果在执行过程中,每次循环都会调用sleep或wait等方法,那么不需要每次迭代都检查是否已中断

先看代码:

public class RightWayStopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
            	//思考为什么没有和上面一样调用Thread.currentThread().interrupt()进行判断?
                while (num <= 10000) {
                    if (num % 100 == 0) {
                        System.out.println(num + "是100的倍数");
                    }
                    num++;
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(3000);
        thread.interrupt();
    }
}

  思考一下在while循环的条件上为什么没有和上面一样调用Thread.currentThread().interrupt()进行判断?其实,上面已经解释过类似的情况了。因为主线程休眠了3秒再对子线程进行中断,而子线程中的这段代码执行速度是非常快的:

if (num % 100 == 0) {
  System.out.println(num + "是100的倍数");
}
num++;

  所以在接收到中断信号时,while循环一定会执行在Thread.sleep(10)这行代码中进行等待,那么就能及时响应中断,抛出异常,中止子线程,就不需要每次循环都判断Thread.currentThread().interrupt()了。

3.2 如果while里面放try/catch,会导致中断失效

public class CantInterrupt {

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

  这里抛出异常后,程序依然正常运行,原因上面也解释过了,抛出异常后,会清除中断标志位,所以正确的做法是要在catch中恢复中断。

四.能响应中断的常用方法

五.停止线程相关的重要函数解析

5.1 判断线程是否已被中断

Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是如果线程处于中断状态,调用一次返回线程中断状态为true,再调用一次会使得这个线程的中断状态重新转为false;
isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。
这里的isInterrupted()是实例方法,调用它需要通过线程对象,interrupted()是静态方法,实际会调用Thread.currentThread()操作当前线程。
来看一道练习题,可以更好的掌握概念:

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());
        //获取中断标志并重置
        System.out.println("isInterrupted: " + threadOne.interrupted());
        //获取中断标志并重直
        System.out.println("isInterrupted: " + Thread.interrupted());
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}

输出如下:

isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true

注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象。也就是对应上面的main线程,main线程并没有中断,所以2和3都输出false。

5.2 interrupt方法原理

  interrupt的设计非常强大,能够让线程处于等待状态还能响应,那这是怎么做到的呢?我们来看看它的实现原理。interrupt里调用了interrupt0方法,是native方法,追溯到hotspot中的c++代码中:   可以看到线程状态是在这里被设置的,然后对一些情况进行唤醒,比如_SleepEvent。SleepEvent就是对应Thread.sleep方法,((JavaThread*)thread)->parker()就是对应LockSupport.park方法,ParkEvent就是对应synchronized同步块及Object.wait方法。

参考书籍: 《深入理解Java虚拟机第三版》