菜单

Juning
发布于 2019-10-21 / 872 阅读
2
0

(三)线程停止、中断的最佳实践

在前面的两篇文章中,我们分析了线程是如何创建、启动的;那么接下来我们就来研究一下算是线程知识的一个小重点——线程的停止和中断。

如何正确停止线程?

说到如何正确的停止线程,我在这里先提出一个提纲挈领的总结:“使用interrupt通知,而不是强制”,这一句话其实涵盖了很多的类容,下面我们来一个一个的将它展开。

那什么是通知,什么又是强制呢?

我们知道,一个线程在被创造、启动之后大部分情况都能直接运行到结束自然停止的,但是在有些情况下我们需要将它提前停止。

什么情况呢?

比如说是用户主动取消、服务需要被快速的关闭以及线程超时或出错,这些情况下我们都需要主动的去停止线程,那么想要让线程安全可靠的快速停止下来并不容易。

Java没有一种机制来安全正确的去停止线程,但是它提供了interrupt,这是一种合作机制。也就是说我们需要用一个线程去通知另一个线程让它停止当前的工作。

所以在Java中最好停止线程的方法就是用interrupt,但是interrupt的中文意思却是“中断”,这点就让我觉得有点懵了,我英语水平虽然差,可我谷歌翻译却用的贼6啊。。。

我们是想要线程停止,而中断这个词明显是不能跟停止画上等号的,那为什么在这里我们却需要将它们看成是同样一个东西呢? 因为在Java中,我们想停止一个线程能做的最多就是只能告诉它:“你,该中断了!”;

那什么时候停止呢?

这就得看需要停止的线程本身了,它不但能决定它什么时候中断,什么时候停止,它还拥有最高的决定权:“停不停止

也就是说我们想去中断一个线程,但是这个线程自身并不想中断。。。

说到这里,OK,我们无能为力,散了散了。。。


这到底是怎么回事呢?,我们想,我们是这个程序的控制者,那凭什么我们没有停止线程的权利呢?

其实我们大多数的时候创建一个线程,一般也都会让它运行到结束;即使是我们将电脑关机,在关机的过程中,电脑也会执行一些保存文件、结束应用程序的收尾工作。

所以其他的线程也是一样的,可能有的时候这个线程也并不是我们自己写的,对它执行的逻辑也不会太清楚,就算想结束掉它,那我们也希望它能够做一些收尾工作再停止,而不是让他立刻停止,也不是让它陷入一种混乱的状态。

因为被停止的线程才最清楚自己的业务逻辑,而通知它去停止的线程并不太清楚被停止线程的逻辑,所以Java在设计之初就将停止的权力和步骤都交给了被停止线程的本身。

想要停止线程,其实是如何正确用interrupt来通知那个线程,以及被停止的线程如何配合,这也是我们停止线程的核心,而不是强制让它停止。

上面说了那么多,理论知识我们是有了,那么接下来我们来实践一下如何正确停止线程:

在实践之前,首先我们还得先知道哪几种情况下线程会停止? 线程只有在两种情况下会停止:

  • run方法的所有代码都运行完毕
  • 有一点异常出现,并且方法中没有将异常捕获

线程在停止后所占用的资源和我们平时创建的对象一样都会被JVM回收

  1. 通常情况下停止线程
/**
 * 描述:run方法内没有sleep或wait方法时,停止线程
 *
 * @author Juning
 * @date 2019/11/14 22:15
 */
public class RightWayStopThreadWithoutSleep implements Runnable {
    //打印出最大整数一半以内所有一万的倍数
    public static void main(String[] args) {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
    }

    @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("任务运行结束。");
    }
}

在上面的代码中我们要打印出最大整数一半以内所有一万的倍数,执行结果如下: 图片.png 可以看到最后的运行结果是1073740000是10000的倍数 下面我们来做一下停止线程的相关操作:

/**
 * 描述:run方法内没有sleep或wait方法时,停止线程
 *
 * @author Juning
 * @date 2019/11/14 22:15
 */
public class RightWayStopThreadWithoutSleep implements Runnable {
    //打印出最大整数一半以内所有一万的倍数
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }

    @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("任务运行结束。");
    }
}

在加了Thread.sleep(500);thread.interrupt();这两行代码之后的运行运行结果如下: 图片.png 最后的运行结果是1073740000是10000的倍数 嗯?跟之前的运行结果一样,运行中的效果也是一样,这又是怎么回事? 实践到这里,我们也验证了之前说的我们只能通知线程去终止,但被停止的线程停止与否,还得看被停止线程的本身 那么我们应该怎么做呢? 因为这段代码是我们自己编写的,那么我们再将代码进行改造,让它能够响应我们发出的interrupt()

/**
 * 描述:run方法内没有sleep或wait方法时,停止线程
 *
 * @author Juning
 * @date 2019/11/14 22:15
 */
public class RightWayStopThreadWithoutSleep implements Runnable {
    //打印出最大整数一半以内所有一万的倍数
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(500);
        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("任务运行结束。");
    }
}

我们在判断num是否是10000的倍数里加上一个条件!Thread.currentThread().isInterrupted(),有了它,我们的线程就可以响应中断了: 图片.png 可以看到,最后的运行结果是133880000是10000的倍数,比我们之前运行的最后结果要少了很多,这就是一个在没有sleep/wait情况下,程序还监听了Thread.currentThread().isInterrupted()之后才能达到停止线程的效果。

  1. 阻塞情况下停止线程
/**
 * 描述:带有sleep中断线程的写法
 *
 * @author Juning
 * @date 2019/11/14 23:08
 */
public class RightWayStopThreadWithSleep implements Runnable {
    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
            }
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务运行结束。");
    }

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

上面的代码和第一点的代码相似,但不同的是我们需要在循环完成后让线程进行休眠一秒钟,然后在它休眠的时候(模仿线程阻塞)去终止它: 图片.png 可以看到在它刚刚开始运行的时候正常的打印出我们想要的结果,但是紧接着程序就抛出了一个异常,那么为什么会出现这个异常呢?

那是因为当程序编写到Thread.sleep(1000);的时候程序会要求我们去处理一个异常,而我选择处理方法是try catch这个异常,于是我们就catch住了java.lang.InterruptedException: sleep interrupted这个异常。

我们接着看Thread.sleep(500);thread.interrupt();这两行代码,我们可以看到如果当线程正在休眠中收到中断信号,那么它便会响应这个中断,它响应中断的方式也特别特殊——抛出一个异常java.lang.InterruptedException: sleep interrupted(在sleep中被打断了),而且还会指出这个异常的位置。

这也是当我们的程序中带有slee或者能让线程进行阻塞方法的时候需要注意的地方,因为当我们需要slee或者让线程阻塞的时候,程序会要求我们将对应的InterruptedException给处理掉,当我们把它处理掉之后就可以做到当线程进入阻塞过程中依然能后响应中断;这也是跟第一点不一样的,当线程休眠中被中断,因为没有try catch,线程收到中断信号也不会抛出异常;

由于sleep方法在我们编程的时候是用的比较多的,即便不是我们例子上显示的那样使用,它也会在很多锁的工具类底层中用使用类似的sleep、wait等方法,所以我们很有必要学习在这种情况下该如何去处理相应的异常。

  1. 在每次迭代后都堵塞
/**
 * 描述:如果在执行过程中,每次循环都会调用sleep或wait等方法,那么不需要每次迭代都检查是否已中断
 *
 * @author Juning
 * @date 2019/11/17 22:36
 */
public class RightWayStopThreadWithSleepEverLoop implements Runnable {

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务运行结束。");
    }

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

}

跟第二点相比,我们只需要将方法进行一个小的修改,运行结果如下: 图片.png 可以看出来,跟第二点的结果几乎类似,前面按照预期的执行,等到阻塞时抛出一个java.lang.InterruptedException: sleep interrupted异常。

那么问题来了,这一点与第二点分区别在哪里呢?

区别在我们是否需要将!Thread.currentThread().isInterrupted()这个条件放下每次循环的判断中。

我们想一下,由于每次循环我们都会执行sleep()方法,但是每次循环的时候都不够长,所以循环大部分时间都是消耗在sleep()方法里面,也就是说我们我们完全可以将这个判断抛去,而且真正判断是不是在休眠被中断的实际上是在休眠中我们去通知它中断sleep()方法所抛出的;

也就是说在迭代的过程中,我们不再需要判断!Thread.currentThread().isInterrupted(),因为程序会在中断的过程中检测中断的状态,并且抛出异常

到这里,interrupt()我们已经了解了,但是还有一种比较神奇的事情——如果while里面放try catch,会导致中断失效,上代码:

/**
 * 描述:如果while里面放try catch,会导致中断失效
 *
 * @author Juning
 * @date 2019/11/17 23:06
 */
public class CantInterrupted {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <= 10000) {
                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();
    }

}

运行结果如下: 图片.png 可以看到运行结果的前几行是和我们之前是一模一样的,到了通知线程停止的时候也抛出了异常,不同点就是程序虽然抛出了异常,但它并没有响应我们通知它终止它就立即停止的情况,而是一直执行到结束!


评论