如何正确停止线程

文章讨论了为什么不应该直接使用stop方法来停止线程,因为它可能导致数据不完整。推荐使用interrupt进行协作停止,让线程在合适的时候优雅退出。在处理阻塞或等待的线程时,需要捕获InterruptedException并重新设置中断标志。volatile在某些情况下可用于停止标志,但不适用于阻塞队列。文中提供了一个例子说明了volatile在生产者消费者模型中可能导致的问题。
摘要由CSDN通过智能技术生成

本文概要:
介绍如何去正确停止一个线程
为什么用 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 被消费
加入到仓库
不需要数据了

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渣渣高不会写Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值