如何正确停止线程

Posted 写Bug的渣渣高

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何正确停止线程相关的知识,希望对你有一定的参考价值。

本文概要:
介绍如何去正确停止一个线程
为什么用 volatile 标记的停止方法可能是错误的?—生产者消费者

为什么不强制停止?

你在学习 stop 方法的时候可能会看到,stop 会让直接停止线程.
但是会发生哪些不好的事情呢, 比如说, 我在写入一个文件, 如果线程突然停止了, 文件输入输出流关闭了吗? 再比如银行系统正在处理 A 给 B 的转账, 如果我们突然把他停止了咋办.
所以说, 很多时候即使是基于特殊情况, 我们也希望让线程把关键的步骤走完再停止, 就像谁愿意去处理离职同事之前的屎山代码呢.

正确的做法是通知, 协作

对于 Java 而言, 我们应该去通知, 也就是使用 interrupt, 他起到的是通知的作用, 对于被通知停止的线程, 它拥有自主权, 他会在合适的时候去停止.
这么做的原因上面也大致谈了谈, 某些业务一定要合理的结束才行, 有始有终, 毕竟谁都不希望数据写一半没有了吧, 这会引起很多问题.

如何去停止线程

public class StopThread implements Runnable 
	@Override
	public void run() 
		int count = 0;
		while (!Thread.currentThread().isInterrupted() && count < 1000) 
		System.out.println("count = " + count++);
		
	
	public static void main(String[] args) throws InterruptedException 
		Thread thread = new Thread(new StopThread());
		thread.start();
		Thread.sleep(5);
		// 这里把开启的那个线程状态设置为 interrupt
		thread.interrupt();
	

每个线程都有标志位, 当我们在其他线程内, 如果持有此线程的对象, 是可以对他进行中断标志位进行设置, 那个线程在 <运行到>这一行代码的时候判断出状态被中断, 然后我们自己手动去让他跳出循环即可.

阻塞, 等待情况分析

有没有注意到上面的三个字 <运行到> ,那么如果是等待或者是阻塞的情况, 就不会运行这一行代码了啊, 还怎么去让他停止

阻塞和等待代表了什么

阻塞: 没有拿到 synchronized 的锁
等待: sleep, wait, join 等

这些方法都会让线程进入一个无法执行代码的状态, 无法判断标志位, 无法通过我们预留的安全通道逃跑, 我们只能用别的办法了.
阻塞和等待都不是陷入一种"死亡"状态, 他们可以感知信号, 所以 Java 开发者是这么去解决阻塞和等待情况下的停止线程问题, 当我们尝试去设置状态的时候, 他们就会抛出错误.

public class SleepThreadExample 
    public static void main(String[] args) 
        Thread thread = new Thread(() -> 
            try 
                System.out.println("Sleeping for 5 seconds...");
                Thread.sleep(5000);
                System.out.println("Woke up from sleep.");
             catch (InterruptedException e) 
                System.out.println("Thread was interrupted while sleeping.");
               // 这里输出一下线程的状态看看是不是被中断了 
                System.out.println(Thread.currentThread().isInterrupted());
                // 这行非常重要
                 Thread.currentThread().interrupt();
            
        );
        thread.start();
        try 
            Thread.sleep(2000);
            thread.interrupt();
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

上面线程在休眠的时候被设置中断标志位了, 这里要注意一点!, sleep 会让线程休眠, 而此时让其中断, 会自动清楚中断信号. 所以说在使用 sleep 的时候, 如果要中断, 一定要注意设置中断标志位. 如果不设置, 那么那个捕获到 try/catch 实际上不会"告诉其他人", 因为中断标志被设置成 false 了.

注意:sleep()方法不会释放锁资源,但是会释放CPU资源。

延申

上面我们使用的方法是, 使用 try/catch 方法去捕捉异常, 如果线程运行的方法是很多个方法, 方法调用方法, 此时该怎么结束.
就比如说我们会调用同事写的代码, 他是用 try/catch 的, 还是用 Exception 的呢?

我们在真实设计的时候, 应该要注意, 线程 run 方法调用的方法是否是 tcy/catch, 因为我们的 run 方法里面也有很多重要的业务, 假如子方法处理的异常, 然后直接抛出运行时异常, 也就是说子方法出错了, 没有告诉 run 方法出错了, run 方法就有可能很多业务还没有处理.

run ()方法, 他不能抛出异常, 只能去通过 try/catch, 而 run ()方法要想控制自己调用的方法, 以及处理可能发生的异常, 最好是能够接收子方法 throw 的异常. 当然如果子方法能够合理的处理异常, 保证异常不会被遗漏, 那么也是可以自己做出处理的, 但是前提是不会影响到 run()的业务处理.
<无论你有没有抛出异常,最重要的应该是,不能去遗漏他,不能屏蔽中断请求>

错误的几种停止方法

比如说 stop (): 会把线程直接停止, 没有给线程足够的时间去保存关键步骤和数据. 想想线程切换的时候, 都会进行线程的上下文的数据保存
suspend (): 不会释放锁, 就是占着茅坑不拉屎, 后面的人只能干等着. 这"河里"吗, 只有当 resumet 的时候才会释放

volatile 用于停止

还记得刚才, 我说 Thread.currentThtread ().interrupt ()会在代码 <运行时>被判断, 然后我们可以手动预留一个安全通道, 让他正确的退出.
volatile 也能用于设置标识位, 但是时什么原理呢? 还是运行时判断, 也就是说阻塞, 等待都不行.

某些情况下, 是可以使用 volatile 来用于停止的, 但是当阻塞的情况下是不适合的.
阻塞的情况, 不止 wait ()和 sleep ()如果是阻塞队列呢?
而合适的情况下, 你可以参考

	public void run() 
		int count = 0;
		while (!Thread.currentThread().isInterrupted() && count < 1000) 
		System.out.println("count = " + count++);
		
	

把这里的! Thread.currentThread().isInterrupted()设置成一个 volatile 变量即可

看看不合适的情况

public class Controller 
    public static void main(String[] args) throws InterruptedException 
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(8);
        Customer customer = new Customer(blockingQueue);
        Producer producer = new Producer(blockingQueue);

        Thread thread = new Thread(producer);
        thread.start();
        Thread.sleep(1000);
        // 这里的 need 是个一个概率问题,也就是说,假如等到消费者不拿了,这里就退出了,然后进入下面去停止生产者,可是生产者在阻塞队列堵住了,无法检测标志
        while (customer.need())
            System.out.println(customer.getStorage().take() + "被消费");
            Thread.sleep(100);
        
        System.out.println("不需要数据了");
        producer.cancer = true;
    


public class Producer implements Runnable
    volatile  boolean cancer = false;
    private BlockingQueue<Integer> storage;

    public Producer(BlockingQueue<Integer> storage) 
        this.storage = storage;
    

    @Override
    public void run() 
        int nums = 0;
        while(nums <= 100000 && !cancer)
            if (nums%50 == 0)
                try 
                    // 假如这里满了,然后就会阻塞住,就无法进入下一个循环来检测 cancer
                    storage.put(nums);
                    System.out.println("加入到仓库");
                 catch (InterruptedException e) 
                    throw new RuntimeException(e);
                
            
            nums++;
        
    




public class Customer 
    private static BlockingQueue<Integer> storage;

    public Customer(BlockingQueue<Integer> storage) 
        this.storage = storage;
    

    public boolean need()
        if (Math.random() > 0.97)
            return false;
        else 
            return true;
        
    

    public BlockingQueue<Integer> getStorage()
        return storage;
    


结果:


1000 被消费
加入到仓库
不需要数据了

然后就停止了, 因为生产者阻塞了, 却没有停止. 程序也没有结束

以上是关于如何正确停止线程的主要内容,如果未能解决你的问题,请参考以下文章

如何正确停止线程

3.线程的八大核心基础知识之如何正确停止线程

如何正确停止线程

如何正确停止线程

JUC多线程:线程的中断停止

JUC多线程:线程的中断停止