【线程基础】如何正确的停止线程

邓敏 2021年12月07日 79次浏览

停止线程有四种方式

方式一 通过volatile标识去判断退出线程

public class VolatileCanStop implements Runnable{

    private static volatile boolean canceled = false;

    @Override
    public void run() {
        int i = 0;
        while (!canceled&&i<=1000){
            i++;
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" "+i+" execution...");
        }
        System.out.println(Thread.currentThread().getName()+" is stop...");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new VolatileCanStop());
        thread.start();
        TimeUnit.SECONDS.sleep(10);
        canceled = true;
    }
}

方式二 使用stop()方法去退出线程

public class StopWay implements Runnable{

    @Override
    public void run() {
        int i = 0;
        while (i <= 1000){
            i++;
            System.out.println(Thread.currentThread().getName()+" "+i+" execution...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+" is stop...");
    }


    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopWay());
        thread.start();
        TimeUnit.SECONDS.sleep(10);
        thread.stop();
    }
}

方法三 suspend() 和 resume() 方法来暂停和恢复线程

public class SuspendWay implements Runnable{
    @Override
    public void run() {
        int i = 0;
        while (true){
            System.out.println(Thread.currentThread().getName()+" "+i+" execution...");
            try {
                i++;
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SuspendWay());
        thread.start();
        TimeUnit.SECONDS.sleep(10);
        thread.suspend();
        System.out.println(thread.getName()+" pause 5 seconds...");
        TimeUnit.SECONDS.sleep(5);
        thread.resume();
        System.out.println(thread.getName()+" recover...");
    }
}

方法四 使用interrupt方法中断线程。

public class InterruptWay implements Runnable{
    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted()&&i<=100){
            System.out.println(Thread.currentThread().getName()+" "+i+" execution...");
            try {
                i++;
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptWay());
        thread.start();
        TimeUnit.SECONDS.sleep(10);
        thread.interrupt();
    }
}

最适合停止线程的方法是哪种呢?

首先我们需要懂一个原则:线程的停止应该是建立在通知和协作上完成,而不是强制停止。

基于这个原则上来看,我们其实就可以排除掉使用 stop() 方法 和 suspend()方法了。

  • 在stop方法中,他是属于暴力强制性去停止线程,本质上是不安全的,当他去停止线程的时候会抛出ThreadDeath异常,这可能会导致线程业务流程不完整的情况,强制关闭线程可能会导致我们无法知道子线程是什么时候关闭的,这时如果需要对线程做资源回收等操作时讲无法进行。

  • 那suspend()方法呢,这个方法其实只起到了暂停线程的作用,后续还可以通过resume来恢复线程的执行。这样的搭配看起来确实没有什么毛病,但是如果使用不当,很容易造成公共的对象的独占,使得其他线程无法访问公共对象,也就是通过suspend()方法停止的线程是不会释放锁的,这样除了不会释放资源,还可能会导致线程中数据不同步的情况。

其实在Java中,官方已经对stop和suspend还有resume方法做了弃用,提醒我们有更好更安全的方法去使用。

stop和suspend不能用,现在我们就只剩下两种方法,一种是通过volatile标识去判断退出线程还有一种是通过使用interrupt方法中断线程。
乍一看,通过volatile标识去判断退出线程好像还挺靠谱的,似乎也是满足是通过通知和协作的方式来让线程来结束的。那他是否可以用在所有的应用场景上呢?
答案是不能的,在上面举例中,他确实可以使用且不会产生毛病,但是接下来我们看下面这种场景。

生产者

public 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) {
                //如果是50的倍数,则放入仓库
                if (num % 50 == 0) {
                    storage.put(num);
                    System.out.println(num + "是50的倍数,被放到仓库中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

消费者

public class Consumer {

    BlockingQueue storage;

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

    public boolean needMoreNums() {
        return !(Math.random() > 0.97);
    }
}

Main方法

public class ExecutorMain {

    public  static  void  main(String[]  args)  throws  InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        //开始执行生产者,从100000收集所有是50倍数的数字
        producerThread.start();
        Thread.sleep(500);
        //开始执行消费者
        Consumer consumer = new Consumer(storage);
        //判断随机数是否大于0.97 大于则跳出,小于则执行消费
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");
        //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
        producer.canceled = true;
        System.out.println(producer.canceled);
    }
}

首先我们来看一下执行的流程,首先是执行生产者,会先从100000个数字中找出是50的倍数,并放入队列中,等待500毫秒后,保障生产者有足够的时间将仓库塞满,当仓库达到容量后就不会在继续塞数据,这个时候生产者就会阻塞,500毫秒后消费者也被创建出来,并通过随机数大于0.97的方式来判断是否需要消费,然后每次消费后休眠100毫秒,当随机数大于0.97时,消费者不再需要数据会跳出循环,之后会将canceled的标记设置为true,并打印输出生产者运行结束。

了解执行流程后发现逻辑好像没有毛病呀,可以是执行几次后会发现,尽管执行到最后已经把canceled设置为true,但生产者依旧没有停止,这是因为在这种情况下,生产者在执行storage.put(num)时发生阻塞,在他被叫醒之前是没有办法进入下一次循环判断canceled的值的,所以在这种情况下用volatile是没有办法将生产者停下来的。

那这个时候就需要通过interrupt来上场了,interrupt方式与volatile标记方式唯一不同的点是即使生产者处于阻塞状态,interrupt仍能感受到中断信号,并做响应处理。

在interrupt使用上,如果interrupt监听到了中断信号,可以通过isInterrupted()方法来判断,或者捕获InterruptedException异常来处理。
这些需要注意的是

  • 再抛出异常时,不要生吞异常。
  • 如果可以处理此异常,请完成清理工作后退出。
  • 不处理此异常,不继续执行任务则需要重新抛出。
  • 不处理此异常,继续执行任务,则需要捕获到异常之后恢复中断标记(交由后续的程序检查中断)

总结

在使用多线程中,需要通过interrupt的方法来去优雅的停止线程,因为这样可以遵循以通知和协作的方式去停止线程,这样也可以使我们对线程的生命周期有全面的把控。