核心三:正确的停止线程

3、核心三:正确的停止线程

3.1 如何正确的停止线程

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

  • 线程中断的机制:一个线程来通知要中断线程(你好,你现在应该停止了)
  • 最后的决定是由要中断程序来执行。如果要中断程序不想停止,那么我们也无能无力
  • 这是合理的,why,我们停止一个线程需要很多步骤(比如,存储停止前线程的状态等)这些步骤由当前线程自己来执行比较好(自己很清楚自己的情况)

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

  1. run方法全部执行完毕(最常见)
  2. 存在异常,方法也没有捕获,线程会停止

3.1.3 正确的停止方法:interrupt(这里会给出不同情况我们应该如何停止线程)

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

/**
 * run方法没有sleep或wait方法时,停止线程
 */
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();
    }
}
//这种情况下,线程被中断后,run方法还是会执行完毕
//因为:方法中没有能够响应中断的机制(注意:线程早就被停止了,只不过run方法还在执行)

正确停止线程的方法

/**
 * run方法没有sleep或wait方法时,停止线程
 */
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(1000);
        thread.interrupt();
    }
}
//这种情况下我们在循环里面加入了判断条件 !Thread.currentThread().isInterrupted()
//此时,run方法能被停止

情况2:线程可能被阻塞

/**
 * 带有sleep的中断线程的用法(该段代码正确的停止线程)
 * sleep方法能够响应中断机制
 */
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);//k
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().isInterrupted());
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);//500的设计是为了实现:中断线程时,我们正在执行k
        thread.interrupt();
    }
}

输出

在这里插入图片描述

看到没有,在调用thread.interrupt()语句时,我们正处于run方法中的Thread.sleep(1000)。此时线程正在被sleep方法阻塞,我们停止线程成功。为什么是成功了呢?明明最后输出false?这个原因看到后面(情况四)就明白了

情况3:如果线程在每次迭代(循环)后都阻塞

/**
 * 每次循环中都有sleep或wait方法,那么不需要每次迭代都检查是否中断(该段代码正确的停止线程)
 *  !Thread.currentThread().isInterrupted()
 */
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 (Exception e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().isInterrupted());
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

输出

在这里插入图片描述

我们成功停止了线程。报错原因和上面一样

情况4:while内try/catch的问题

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

输出

在这里插入图片描述

我们停止线程失败了。因为Java语言在设计sleep函数的时候,有这样的一个理念。就是,当他一旦响应中断,便会把线程的interrupt标记位给清除。也就是说,在刚刚的情况下,它确实在sleep过程中收到了中断,而且也catch并打印出异常信息。但是,由于sleep设计的理念,导致interrupt标记位被清除。所以我们每次循环判断Thread.currentThread().isInterrupted()都会返回false。还有看看try_catch放在循环里面,catch打印异常信息之后,循环继续😂

正确的停止这种情况下的线程方法在后面

情况5:实际开发中的两种最佳实践💟

优先选择:传递中断 1

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

不应屏蔽中断(吞了中断信息,例如第一种实践中的try语句),这种情况非常不可取

  1. 第一种最佳实践(优先选择:传递中断)

    这里的传递就是指异常的传递,即throws异常

    /**
     * 最佳实践(最好的停止线程方法):catch了InterruptedExcetion之后的优先选择:
     * 在方法签名中抛出异常
     * 那么在run方法中就会强制要求try/catch
     
     * 下面的代码是错误的示范
     */
    public class RightWayStopThreadInProd implements Runnable{
        @Override
        public void run () {
            while(true){
                System.out.println("go");
                throwInMethod();
            }
        }
    
        private void throwInMethod(){
            try {
                Thread.sleep(2000);//k
            } catch (InterruptedException e) {
                //在执行k时,我们收到中断信号。此时我们代码在此处进行了消极处理:
              //try/catch将中断信号吞了(没有上报领导run,导致领导没有收到中断通知,中断失败)
                e.printStackTrace();
            }
        }
    
        public static void main (String[] args) throws InterruptedException {
            Thread thread = new Thread(new RightWayStopThreadInProd());
            thread.start();
            Thread.sleep(1000);
            thread.interrupt();
        }
    }
    

    输出

    在这里插入图片描述

    我们中断成功了。只不过run还没有停止执行。判断Thread.currentThread().isInterrupted()都会返回false。

    处理方法:try/catch改为throws

    public class RightWayStopThreadInProd implements Runnable{
        @Override
        public void run () {
            while(true && !Thread.currentThread().isInterrupted()){
                System.out.println("go");
                try {
                    throwInMethod();
                } catch (InterruptedException e) {
                    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();
        }
    }
    

    输出

    在这里插入图片描述

    看到了没,我们可以正确处理响应中断请求。我们中断成功了。只不过run还没有停止执行。判断Thread.currentThread().isInterrupted()都会返回false。

  2. 第二种最佳实践(不想或无法传递:恢复中断)

    /**
     * 最佳实践2:在catch语句中调用Thread.currentThread().interrupt()来恢复中断状态
     * 以便于在后续的执行中,依然能够检查到刚才发生了中断
     * 回到刚才RightWayStopThreadInProd补上中断,让他跳出run
     */
    public class RightWayStopThreadInProd2 implements Runnable{
        @Override
        public void run () {
            while(true){
                System.out.println("1");
                if(Thread.currentThread().isInterrupted()) {
                    System.out.println("程序运行结束");
                    break;
                }
                reInterrupt();
            }
        }
    
        private void reInterrupt() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();//这条语句解决了try语句独吞中断信号
                e.printStackTrace();
            }
        }
    
        public static void main (String[] args) throws InterruptedException {
            Thread thread = new Thread(new RightWayStopThreadInProd2());
            thread.start();
            Thread.sleep(2000);
            thread.interrupt();
        }
    }
    

    输出

    在这里插入图片描述

    我们正确的停止了线程,并且停止了run方法的继续执行

    现在为了显示Thread.currentThread().interrupt();这条语句的重要性。我们注释掉他,结束输出:

    在这里插入图片描述

    这条语句是解决try语句独吞中断信号的情况,导致run知道中断信息,以便于停止run方法

情况6:响应中断的方法总结列表

响应中断表示:当我们执行wait()方法时,如果有中断信号过来了我们可以知道中断过来了

例如 Thread.sleepObject.wait 等,都会检查线程何时中断,并且在发现中断时提前返回。他们在响应中断时执行的操作包括:清除中断状态,抛出 InterruptedException ,表示阻塞操作由于中断而提前结束。
在这里插入图片描述

在这里插入图片描述

3.1.4 正确停止带来的好处(使用interrupt来中断的好处)

被中断的线程拥有控制是否中断的权限。这一点非常重要,被中断的线程自己最了解自己,数据的备份、线程状态的存储自己亲自来进行比较合理。这样的做法我们保证了线程的安全和完整。

3.2 interrupt()源码分析💟

再次强调,该方法只是通知一下(设置线程的中断标志为true),不是强制执行

public class Thread implements Runnable {
    public void interrupt() {
        if (this != Thread.currentThread()) {
            checkAccess();

            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupted = true;
                    interrupt0();  // inform VM of interrupt
                    b.interrupt(this);
                    return;
                }
            }
        }
        interrupted = true;
        // inform VM of interrupt
        interrupt0();
    }
    //关键就在这里,但这是C++的代码。老师在彩蛋里面有明确的说明:如何查看native代码
    private native void interrupt0();
}

判断是否已被中断相关方法

public static boolean interrupted() 1

public boolean isInterrupted() 2

  • 作用:检测当前线程是否被中断
  • 方法1,在返回结果时,直接清除线程的中断状态(注意:一个线程被清除中断状态后再次调用方法判断是否中断时,返回false(除非当前线程在第一次调用清除其中断状态之后和第二次调用检查它之前再次中断)
  • 方法2,返回目标线程的中断状态
  • 方法1,他的目标对象应该是当前线程(执行这条语句的线程)。而不是方法调用者

源码:

  1. static boolean interrupted()

    /**
    * 测试当前线程是否已中断。此方法清除线程的 中断状态 。换言之,如果连续调用此方法两次,则第二次调* 用将返回 false(除非当前线程在第一次调用清除其中断状态之后和第二次调用检查它之前再次中断)。
    * 返回:true 如果当前线程已中断; false 否则。
    */
    public static boolean interrupted() {
        //true,说明清除中断标志
        return currentThread().isInterrupted(true);
    }
    
  2. boolean isInterrupted()

    public boolean isInterrupted() {
        //false,说明不清除中断标志
        return isInterrupted(false);
    }
    

练习

/**
 * 注意Thread.interrupted()方法的目标对象是“当前线程 ”
 * 而不管本方法是由谁来调用的
 */
public class RightWayInterrupted {
    public static void main (String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run () {
            }
        });
        thread.start();
        thread.interrupt();
        System.out.println(thread.isInterrupted());
        System.out.println(thread.interrupted());//此方法,处理对象是执行这条语句的线程main
        System.out.println(Thread.interrupted());//此方法,处理对象是执行这条语句的线程main
        System.out.println(thread.isInterrupted());
    }
}

输出

true
false
false
true

3.3 错误的停止方法(两种)

被弃用的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();//此时停止线程会发生错误
    }
}

输出

联队0开始领取武器
0
1
2
3
4
5
6
7
8
9
联队0领取武器了
联队1开始领取武器
0
1
2
3
4
5

我们发放弹药是以连队为单位的,错误的停止线程,使联队1中有人领取了,有人没有领取。这样的结果是大忌。千万要避免

注意事项:

  1. 关于stop理论错误的说法:不使用stop方法,是因为该方法不会释放所有的监视器
  2. suspend不会释放锁,可能会造成死锁

用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 aVolatile = new WrongWayVolatile();
        Thread thread = new Thread(aVolatile);
        thread.start();
        Thread.sleep(5000);
        aVolatile.canceled = true;
    }
}

输出:

在这里插入图片描述

在这种情况下,我们确实实现了线程的终止

但是,还有一种情况,我们没有实现停止线程的任务。在《并发编程实战》中对这种情况描述为:

如果使用这种方法(用volatile设置boolean标记位)的任务调用了一个阻塞方法,例如 BlockingQueue.put,那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,因此永远不会结束

/**
 * 演示用volatile的局限性:陷入阻塞时,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(10);

        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{
    BlockingQueue storage;//存放生产物品的仓库
    public volatile boolean canceled = false;

    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;
    }
}

结果

在这里插入图片描述

原因 :

一旦,队列满了,put方法也会被阻塞。当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,那消费者使用producer.canceled = true;。那么就会发生上面的结果。因为生产者无法从阻塞的put方法中恢复过来(因为队列满了并且消费者停止了)

一旦,仓库满了storage.put(num);就不会执行下面的所有代码。因此,我们线程停止失败了

while (num<=100000 && !canceled){
    if(num % 100 == 0){
        storage.put(num);
        System.out.println(num+"是100的倍数,它已经被放到仓库中");
    }
    num++;
}

我们看看这个方法的具体实现

在这里插入图片描述

对于上面的生产者消费者模型,我们使用正确的停止线程的方式来终止线程

/**
 * 用中断来修复刚才的无尽等待问题
 */
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;
        }
    }
}

输出

在这里插入图片描述

3.4 常见面试题

如何停止线程

  1. 使用interrupt来请求中断,好处:保证数据安全,主动权在被中断的线程手上
  2. 要想成功中断,需要三方相互配合:请求方、被停止方、子方法被调用方
  3. 最后再说错误中断的方法:stop/suspend已弃用,volatile的boolean无法处理长时间阻塞的情况

如何处理不可中断的阻塞

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

3.5 关于run方法不能抛出异常的讨论

看代码

在这里插入图片描述

此时IDEA报错

‘run()’ in ‘Anonymous class derived from java.lang.Runnable’ clashes with ‘run()’ in ‘java.lang.Runnable’; overridden method does not throw ‘java.lang.Exception’

我们只能try

在这里插入图片描述

原因,请看下面的源码

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

如有不对的地方请在评论区指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值