【Kill Thread Part.1-3】线程停止、中断最佳实践

【Kill Thread Part.1-3】线程停止、中断最佳实践

  • 讲解原理
  • 最佳实践:如何正确停止线程
  • 停止线程的错误方法
  • 重要函数的源码解析
  • 常见的面试问题

一、涵盖内容

image-20220119184100108

二、原理讲解

原理介绍:使用interrupt来通知,而不是强制

我们只能用interrupt来通知线程,你该中断了,但是并不能强制线程中断,是否中断的决定权,在线程自己的手里。

Java认为,被停止的线程本身要更清楚自身的工作和运行状态,而我们在外部强制中断的话,并不是非常了解被中断线程的各个状态。

三、最佳实践:如何正确停止线程

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

  • run()方法运行完毕
  • 有异常出现,并且方法中没有捕获异常,线程停止,线程的资源被JVM回收

2、正确的停止方法:interrupt

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

测试代码

public class RightWayStopThreadWithoutSleep implements Runnable{

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

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(2000);
        //发送中断信号
        thread.interrupt();
    }
}

②线程可能被阻塞

测试代码
/**
 * 描述: 带有sleep的中断线程的写法
 * 打印出100的倍数,完成任务之后,需要做一个等待,在等待的时候,线程被中断
 */
public class RightWayStopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            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();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        //中断
        thread.interrupt();
    }
}
运行结果

image-20220119190931354

结果分析
  • 在代码中有可以让线程阻塞的方法,比如sleep()。必然会需要我们处理InterruptedException这样的异常

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

测试代码
/**
 * 描述: 如果在执行过程中,每次循环都会调用sleep或者wait等方法,那么不需要每次迭代都检查是否已中断
 */
public class RightWayStopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                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(5000);
        //中断
        thread.interrupt();
    }
}
运行结果

image-20220119191526890

结果分析
  • 在每一次迭代,都会阻塞的时候,我们就没有必要在while循环中去判断是否有阻塞信号
  • 因为在sleep()方法会有代码帮助我们响应这样的中断

③阻塞方法在while循环里面,try/catch也在while循环里面,没有报过循环

测试代码
/**
 * 描述: 如果while里面放try/catch,会导致中断失效
 */
public class CantInterrupt {
    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();
    }
}
运行结果

image-20220119192250510

发现这个线程没有被中断,在抛出异常之后,还再继续执行。

结果分析
  • 通过上述的代码方式,我们可以看出,循环终止的条件并没有改变,只是在一次循环中抛出了这个异常,所以线程没有被中断。
  • 如果在while循环的判断条件中加上是否已经停止的判断,仍然无法中断线程,这是为什么呢?
    • 这是因为,在sleep()方法结束之后,会把线程的interrupted标志位清除,所以下一次循环判断的时候,标志位还是为0,则可以继续正常运行。

3、实际开发中的两种最佳实践

①优先选择:传递中断

糟糕的情况
/**
 * 描述; 最佳实践:catch了InterruptedException之后
 * 优先选择在方法签名中抛出异常
 * 那么run()方法就会强制try/catch
 */
public class RightWayStopThreadInProd implements Runnable{
    @Override
    public void run() {
        //即使这里做了判断,但是在sleep()之后,会把interrupted标志位擦除。
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            throwInMethod();
        }
    }

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

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

在这种情况下,我们的业务逻辑都在run()方法中,但是我们调用了一个其他小伙伴写的方法,这时候有一个线程想要中断我们这个线程,但是我们写的代码却无法处理和响应中断,这就是比较致命了。

最佳实践

image-20220119193800293

注意,run方法无法再向上抛出异常,因为run方法是一个重写的方法,是Runnable接口定义好的

image-20220119193949803

由于定义的时候就没有抛出,所以此时不允许我们再往上抛异常。在run方法里只能够try/catch

最佳实践处理代码
/**
 * 描述; 最佳实践:catch了InterruptedException之后
 * 优先选择在方法签名中抛出异常
 * 那么run()方法就会强制try/catch
 */
public class RightWayStopThreadInProd implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                //保存日志、停止程序等操作来响应中断
                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();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

②不想或无法传递:恢复中断

测试代码
/**
 * 描述: 最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,
 * 以便于在后续的执行中,依然能够检查到刚才发生了中断
 * 回到刚才的RightWayStopThreadInProd补上中断,让它跳出
 */
public class RightWayStopThreadInProd2 implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("go");
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("线程中断!Interrupted,程序运行结束");
                //保存日志、停止程序等操作来响应中断
                System.out.println("保存日志!");
                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();
    }
}
运行结果

image-20220119195505006

结果分析

以上做法说明,我们如果想到在别的方法中处理中断信号,还需要重新抛出中断信号,不应该把中断信号自己独吞。

③不应屏蔽中断

屏蔽中断:

  • 即不在方法签名中抛出,也不在catch语句中恢复中断。
  • 如果被调用的方法屏蔽了中断,会造成信息传递的不畅通。

4、会响应中断的方法总结列表

image-20220119195959182

  • 前三个方法说的是,这三个方法执行的过程当中,是能够感知到中断信号的。

除此之外还有下列的方法:

补充:Java异常体系

image-20220119200227389

  • Error指的是程序内部错误或者资源耗尽,意味着我们在代码层面无法catch这些错误。
  • Exception发生了RuntimeException一般都是程序员自己的问题(程序问题)称为非受检查异常,也就是unchecked Exception
  • 出了RuntimeException之外的其他Exception,都是受检查异常,可以抛出或者检测的,可以在程序中提前处理。提高健壮性。

四、停止线程的错误方法

1、被弃用的stop、suspend和resume方法

测试代码
/**
 * 描述;  错误的停止方法:用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();
    }
}
运行结果

image-20220119210612898

  • 发生了数据错乱,如果我们是银行转账的线程,则会发生很大的问题,造成严重的后果,涉及到这样的问题,需要我们至少让这个线程把业务逻辑的最小执行单元运行完毕,然后再中断。
  • image-20220119210940334
  • suspend会把线程挂起,是带着锁休息的,容易造成线程发生死锁。resume方法会唤醒挂起的方法。

2、用volatile设置boolean标记位

①看上去可行

可行的代码
/**
 * 描述:演示用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;
    }
}

程序运行之后,过了五秒钟确实是停止了。

②错误原因

测试代码
/**
 * 描述:     演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的 此例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
 */
public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");

        //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况
        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 {
            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;
    }
}

通过生产者消费者的案例,现实中,生产者的生产速度是很快的,但是消费者的消费速度很慢,因为要进行一些数据处理等等。

运行结果

image-20220119213549167

消费者不需要数据了,但是我们的线程还没有停止。

错误原因分析
  • 失效发生的点是产生阻塞
  • image-20220119213712627
  • 所以根本没有捕获到中断的信号
  • 这就是我们要使用interrupted的原因,因为Java设计者已经考虑到了sleep(),wait()这些阻塞方法对于中断的影响。所以当我们使用interrupt方法的时候,就可以通知线程去中断。

③修正方式

测试代码
/**
 * 描述:     用中断来修复刚才的无尽等待问题
 */
public class WrongWayVolatileFixed {

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        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(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");


        producerThread.interrupt();
    }


    class Producer implements Runnable {

        BlockingQueue storage;

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


        @Override
        public void run() {
            int num = 0;
            try {
                //检查是否被中断了
                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;
        }
    }
}

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

1、interrupt方法

①Java源码

image-20220119214509979

image-20220119214528785

已经深入到了native方法,我们该怎么去看C++代码呢?

②interrupt0 C++代码分析

  • 进入github找到openjkd

  • https://github.com/openjdk-mirror/jdk7u-jdk/find/master
    
  • image-20220119214934461

  • 点进去

    • image-20220119215017379

找到了interrupt0在JVM中的名字

  • 找jvm的实现

  • https://github.com/openjdk-mirror/jdk7u-hotspot/search?q=JVM_Interrupt
    
  • image-20220119215213024

  • 找到jvm.cpp这个类

  • image-20220119215405346

  • 继续在这个类中找到函数

  • image-20220119215352080

  • 继续往下找Thread.cpp

    • image-20220119215516825
    • 找到了对应的方法,发现调用了os::interrupt()

③os::interrupt方法

image-20220119215625507

2、判断是否已经被中断的相关方法

①static boolean interrupted() --- 检测线程是否被中断

  • 此方法返回布尔值之后,会把中断的状态直接设置为false。直接把线程的中断状态interrupted标志位清除了。
  • 这个方法也是唯一能够清除线程中断标志位的办法。

源码分析:

image-20220119220127696

②boolean isInterrupted() --- 检测线程是否被中断

image-20220119220215910

这个方法不清除标志位。

③Thread.interrupted()的目的对象

注意:Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于那个对象。

测试代码
/**
 * 描述:     注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象
 */
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.");
    }
}
运行结果

image-20220119220834961

结果分析

第二个和第三个中断:threadOne.interrupted()执行这个的时main线程,main线程没有被中断所以返回false

六、面试常见问题

1、如何停止线程

image-20220119221215948

2、如何处理不可中断的阻塞

  • 特定情况特定的方法,尽可能的让它响应中断。

image-20220119221509206

posted @ 2022-01-19 22:16  DarkerG  阅读(97)  评论(0编辑  收藏  举报