Java并发编程实战 07 | 如何正确停止线程

news2024/12/23 5:53:45

什么时候需要停止一个线程?

一个线程被创建并启动之后,大部分情况下都会自然运行至结束,但是也有一些情况需要主动停止线程,比如:

  1. 用户主动取消执行:用户可能会中止一个正在进行的操作,这时需要停止相关线程。
  2. 运行时错误或超时:线程可能因为运行时错误或超时而需要被停止,以避免长时间占用资源。
  3. 服务关闭:当服务即将关闭时,可能需要停止所有正在运行的线程,以释放资源并确保干净地关闭服务。

所有这些情况都需要主动停止线程,但是要安全可靠地停止线程并不是一件容易的事情。

为什么不能强制停止线程?

实际上,当我们停止一个线程时,通常希望它至少能完成一些必要的收尾工作,如保存数据、切换状态等,而不是立即停止,以免导致状态混乱。

生活中,我们经常会遇到类似的情况。例如,当你将文件从电脑剪切并粘贴到U盘时,如果在传输过程中突然中断,你将面临一个问题:部分文件已经被复制到U盘,而另一部分还留在电脑上。这种情况下,你需要恢复到原始状态,避免出现一半的文件在U盘上,而另一半还在电脑里。

因此在处理文件传输或其他重要操作时,线程停止时我们需要确保所有操作都能完整地执行完毕,避免出现不完整或数据不一致的状态。

如何正确停止一个线程?

Java 语言本身并没有提供一种机制可以保证线程立即正确停止,但是它提供了 interrupt() 方法,这是一种协作机制,可以用于停止线程。

interrupt() 方法并不会直接终止线程,而是设置线程的中断状态。在线程中应该定期检查它的中断状态,并响应这个中断请求。被中断的线程拥有决定权,即它可以选择何时停止运行,甚至可以选择忽略这个中断请求。换句话说,如果一个线程不愿意响应中断请求,那么我们除了等待它完成任务或强制终止整个进程之外,别无他法。

如果被中断的线程正在等待、睡眠或被阻塞(例如,在调用 Thread.sleep() 或 wait() 等方法时),会立即抛出 InterruptedException 异常。线程可以捕获这个异常,并在异常处理程序中做出适当的响应(如清理资源、记录日志或退出线程)。

代码实践

使用stop()停止线程

使用stop()方法终止线程执行将导致线程立即停止,可能会引发意想不到的问题。

public class StopThread implements Runnable {

    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            //Simulation of time required to move
            int j = 50000;
            while (j > 0) {
                j--;
            }
            System.out.println(i + " batches have been moved");
        }
        System.out.println("End of moving");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        // Try to stop it later.
        Thread.sleep(2);
        thread.stop();
    }
}

//输出:
Start moving...
1 batches have been moved
2 batches have been moved
3 batches have been moved

可以看到,使用 stop 方法强制结束线程可能会导致操作不完全:上面的例子中,只有三批物品被移动,而这些物品在停止后没有被移回原处,这种情况可能带来数据不一致的问题。

由于这种强制停止线程的方式可能导致不稳定和无法预料的结果,stop 方法已经被官方弃用,并在源代码中标记为过时。出于安全考虑,建议使用其他更安全的方式来管理线程的中断和终止。

使用interrupt方法,线程不停止

在主线程中调用 interrupt 方法来中断目标线程时,目标线程可能无法感知到中断标志,也就是说,即使主线程发出了中断请求,目标线程可能继续运行,不会及时停止或做出其他响应。这种情况可能会导致线程无法按照预期停止,从而影响系统的稳定性和性能。

public class InterruptThreadWithoutFlag implements Runnable {

    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            //Simulation of time required to move
            int j = 50000;
            while (j > 0) {
                j--;
            }
            System.out.println(i + " batches have been moved");
        }
        System.out.println("End of moving");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        // a little later
        Thread.sleep(2);
        thread.interrupt();
    }
}

//输出:
Start moving...
1 batches have been moved
2 batches have been moved
3 batches have been moved
4 batches have been moved
5 batches have been moved
End of moving

你可能会发现,调用 interrupt 方法对线程没有任何效果。我们本来希望通过中断来停止线程,但它似乎完全忽视了我们的请求。

正如前面所提到的,是否响应中断信号取决于线程自身。为了确保线程能够响应中断,我们需要修改线程的逻辑,使其能够处理中断请求。这样,线程才能在接收到中断信号后及时停止。

使用interrupt,线程识别中断标志

当一个线程被中断时,线程内部可以通过调用 Thread.currentThread().isInterrupted() 方法来检查中断状态。如果该方法返回 true,则表示线程已经收到中断信号,线程可以据此执行相应的中断处理逻辑。

public class InterruptThread implements Runnable {

    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            if(Thread.currentThread().isInterrupted()) {
                //Do some finishing work.
                break;
            }
            //Simulation of time required to move
            int j = 50000;
            while (j > 0) {
                j--;
            }
            System.out.println(i + " batches have been moved");
        }
        System.out.println("End of moving");
    }

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

//输出:
Start moving...
1 batches have been moved
End of moving

查看打印输出后,我们可以发现,interrupt() 方法中断线程已经生效了。

中断某个线程时,该线程处于休眠状态

如果在线程处理中调用了 sleep 方法,即使线程未显式检查中断标志,它也会响应中断信号。例如,我们可以使用 Thread.sleep(1) 模拟每次搬运操作的时间,在主线程中等待 3 毫秒后进行中断,因此预计在搬运 2 到 3 批物品后线程会被中断,代码如下:

public class InterruptWithSleep implements Runnable {

    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            // 模拟移动所需的时间
            try {
                Thread.sleep(1);
                System.out.println(i + " batches have been moved");
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
                break;
            }
        }
        System.out.println("End of moving");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptWithSleep());
        thread.start();
        // 稍等片刻
        Thread.sleep(3);
        thread.interrupt();
    }
}

//输出:
Start moving...
1 batches have been moved
2 batches have been moved
sleep interrupted
End of moving

这里还输出了 “sleep interrupted”,这是因为发生了中断异常。使程序执行到了 catch (InterruptedException e) 语句块,通过 e.getMessage() 输出了这个信息。

为什么会抛出异常呢?

这是因为当线程处于睡眠状态时,如果接收到中断信号,线程会立即响应这个中断。而响应中断的方式很特别,就是抛出一个异常:java.lang.InterruptedException: sleep interrupted。这确保了线程在睡眠时能够快速地对中断请求做出反应。

因此,当程序中有使用 sleep 或其他可能阻塞线程的方法(如 wait、join 等)时,如果这些方法可能会被中断,就需要特别注意 InterruptedException 异常的处理。我们可以在 catch 块中捕获这个异常,这样当线程进入阻塞状态时,仍然能够响应中断并执行相应的处理逻辑。

当sleep方法与isInterrupted一起使用时会发生什么情况?

大家有没有注意到,在前面的代码中,在捕获异常之后的处理中,我们使用了break主动结束这个循环。那么,我们能不能不用break,而是在循环入口处使用isInterrupted()判断?这样看起来更自然一些。让我们尝试一下:

public class SleepWithIsInterrupted implements Runnable {

    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            if(Thread.currentThread().isInterrupted()) {
                //Do some finishing work.
                break;
            }
            //Simulation of time required to move
            try {
                Thread.sleep(2);
                System.out.println(i + " batches have been moved");
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.println("End of moving");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepWithIsInterrupted());
        thread.start();
        // a little later
        Thread.sleep(1);
        thread.interrupt();
    }
}

//输出:
Start moving...
sleep interrupted
2 batches have been moved
3 batches have been moved
4 batches have been moved
5 batches have been moved
End of moving

输出结果有点出乎意料?中断之后怎么还继续移动第四批和第五批物品呢?

原因是一旦sleep()响应中断,就会清除线程的中断状态标志位,所以上面代码中的循环条件检查中,Thread.currentThread().isInterrupted()的结果一直是false,导致程序无法退出。

一般情况下,在实际业务代码中,主逻辑通常比较复杂,所以不建议在try-catch这里直接处理这个中断异常,而是直接将异常向上抛出,交由更高层次处理,便于功能分解和团队协作。你可以把把中断的后续收尾处理封装成一个方法,如下:

public class SleepSplitCase implements Runnable {

    @Override
    public void run() {
        try {
            move();
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
            goBack();
        }
    }

    private void move() throws InterruptedException {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            //Simulation of time required to move
            Thread.sleep(2);
            System.out.println(i + " batches have been moved");
        }
        System.out.println("End of moving");
    }

    private void goBack() {
        // do some finishing work.
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new SleepSplitCase());
        thread.start();
        // a little later
        Thread.sleep(1);
        thread.interrupt();
    }
}

//输出:
Start moving...
sleep interrupted

重新中断

有没有办法让 goBack 方法在 catch 块之外处理?

如前所述,当中断发生并抛出 InterruptedException 时,isInterrupted 的结果会被重置为 false。不过,可以通过再次调用 interrupt 方法来重新设置中断标志,这样 isInterrupted 的结果会变为 true。

基于这个前提,run 方法可以改为如下形式:

@Override
public void run() {
    try {
        move();
    } catch (InterruptedException e) {
        System.out.println(e.getMessage());
        Thread.currentThread().interrupt();
    }
    if (Thread.currentThread().isInterrupted()) {
        goBack();
    }
}

这样就避免了在catch代码块中处理业务逻辑!

确定是否发生中断的方法

  • boolean isInterrupted():用于检查当前线程是否被中断。这个方法会检查线程的中断状态,但不会清除中断标志。
  • static boolean interrupted():用于检查当前线程是否被中断。调用此方法后,它会将当前线程的中断标志重置为 false,即清除中断标志。

注意,interrupted() 方法针对的是当前线程,从源代码中可以很容易地看到:

我们来看下面的例子。我添加了一些注释来帮助理解:

public class CheckInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Thread subThread = new Thread(() -> {
            
            for (; ; ) {
            }
        });

        subThread.start();
        subThread.interrupt();
        //获取中断标志
        System.out.println("isInterrupted: " + subThread.isInterrupted());
        //获取中断标志并重置
        // 虽然interrupted()是subThread线程调用的,但实际执行的是当前线程)  
        System.out.println("isInterrupted: " + subThread.interrupted());

        //中断当前线程 
        Thread.currentThread().interrupt();
        System.out.println("isInterrupted: " + subThread.interrupted());
        // Thread.interrupted() 与 subThread.interrupted() 的效果是相同的。
        System.out.println("isInterrupted: " + Thread.interrupted());
    }
}

// 输出:
isInterrupted: true
isInterrupted: false
isInterrupted: true
isInterrupted: false

因为interrupted()会重置中断标志,所以最后的输出结果为false。

Jdk中响应中断信号的方法列表

JDK 有一系列内置方法可以响应中断信号 这些方法主要包括以下几种,它们会响应中断并抛出 InterruptedException:

Object.wait() / wait(long) / wait(long, int)
Thread.sleep(long) / sleep(long, int)
Thread.join() / join( long) / join(long, int)
java.util.concurrent.BlockingQueue.take() / put (E)
java.util.concurrent.locks.Lock.lockInterruptibly()
java.util.concurrent.CountDownLatch.await
java.util.concurrent.CyclicBarrier.await
java.util.concurrent.Exchanger.exchange(v)
Related methods of java.nio.channels.InterruptibleChannel
Related methods of java.nio.channels.Selector

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2116892.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python系统教程004(字符串)

一、input函数的算术运算 一包奥特曼卡片卖0.5元&#xff0c;小华想编写一个只需要输入卡片的包数就能自动计算价格的程序&#xff0c;请你帮帮它。 解题 报出类型错误 注意&#xff1a; input函数接收到的键盘信息&#xff0c;默认都是字符串的数据类型。 字符串的数据类型…

书生浦语三期实战营 [进阶] LMDeploy 量化部署进阶实践

LMDeploy 量化部署进阶实践 1 配置LMDeploy环境 1.1 InternStudio开发机创建与环境搭建 在终端中&#xff0c;让我们输入以下指令&#xff0c;来创建一个名为lmdeploy的conda环境&#xff0c;python版本为3.10&#xff0c;创建成功后激活环境并安装0.5.3版本的lmdeploy及相关…

IM即时通讯,稳定可靠的即时通讯服务-WorkPlus

在现代企业日常工作中&#xff0c;即时通讯已成为了一种不可或缺的沟通工具。为了满足企业对稳定可靠的即时通讯服务的需求&#xff0c;WorkPlus提供了一款优秀的IM即时通讯平台&#xff0c;以满足企业高效沟通和协作的要求。本文将深入探讨IM即时通讯服务的重要性以及WorkPlus…

Linux下的RTC应用

RTC RTC基础知识 1. RTC简介 RTC 全称是 Real-Time clock&#xff0c;翻译过来是实时时钟。实时时钟在日常生活中的应用也比较泛&#xff0c;比如电子时钟。实时时钟可以为系统提供精确的实时时间&#xff0c;通常带有电池&#xff0c;可以保证系统断电时还可以正常工作&…

JVM虚拟机 - 基础篇

一、初始JVM 1. JVM是什么 2. JVM的三大核心功能是什么&#xff1f; 3. 常见的JVM虚拟机有哪些&#xff1f; 二、字节码文件详解 1. Java虚拟机的组成 2. 字节码文件的组成 &#xff08;1&#xff09;基本信息 Magic魔数 主副版本号 &#xff08;2&#xff09;常量池 &#…

RabbitMQ练习(AMQP 0-9-1 Overview)

1、What is AMQP 0-9-1 AMQP 0-9-1&#xff08;高级消息队列协议&#xff09;是一种网络协议&#xff0c;它允许遵从该协议的客户端&#xff08;Publisher或者Consumer&#xff09;应用程序与遵从该协议的消息中间件代理&#xff08;Broker&#xff0c;如RabbitMQ&#xff09;…

Day19_0.1基础学习MATLAB学习小技巧总结(19)——MATLAB绘图篇(2)

利用空闲时间把碎片化的MATLAB知识重新系统的学习一遍&#xff0c;为了在这个过程中加深印象&#xff0c;也为了能够有所足迹&#xff0c;我会把自己的学习总结发在专栏中&#xff0c;以便学习交流。 参考书目&#xff1a;《MATLAB基础教程 (第三版) (薛山)》 之前的章节都是…

H5漂流瓶社交系统源码

一个非常有创意的H5漂流瓶社交系统源码&#xff0c;带完整前端h5和后台管理系统。 环境&#xff1a;Nginx 1.20.1-MySQL 5.6.50-PHP-7.3 代码下载

python简单计算入门教程|加减法

python通过调用numpy模块&#xff0c;非常擅长数学计算。再通过调用matplotlib模块&#xff0c;可以自由自在地输出numpy计算的结果。 今天&#xff0c;我们就尝试一些基本计算。 下述是正弦函数和余弦函数的加法和减法计算结果。 图1 代码为&#xff1a; import matplotli…

【stata】处理城市名和城市代码

写了两个简单的外部命令&#xff0c;在这里分享一下&#xff0c;希望能帮到大家 1.citycode_mutate 第一个命令是citycode_mutate&#xff0c;用于识别字符串中可能存在的城市信息&#xff0c;并生成城市代码&#xff08;图1图2&#xff09;。 2.cityname_mutate 第二个命令…

如何编写Linux PCIe设备驱动器 之二

如何编写Linux PCIe设备驱动器 之二 功能(capability)集功能(capability)APIs通过pci_bus_read_config完成功能存取功能APIs参数pos常量值PCI功能结构 PCI功能IDMSI功能电源功率管理功能 功能(capability)集 功能(capability)APIs int pcie_capability_read_word(struct pci_…

C++(一)----C++基础

1.C的发展史 C语言诞生后&#xff0c;很快普及使用&#xff0c;但是随着编程规模增大且越来越复杂&#xff0c;并且需要高度的抽象和建模时&#xff0c;C语言的诸多短板便表现了出来&#xff0c;为了解决软件危机&#xff0c;上世纪八十年代&#xff0c;计算机界提出了oop&…

拓扑排序-广度优先遍历思路

本质&#xff1a; 【广度优先遍历 】【贪心算法】应用于【有向图】的专有名词 应用场景&#xff1a;任务调度&#xff0c;课程安排 作用&#xff1a; 得到一个不唯一的【拓扑序】检测【有向图】是否有环&#xff0c;使用数据【并查集】 使用&#xff1a;先找度为0的前驱节点…

Linux运维排查常见故障_在tmp目录下有大量包含picture_ 的临时文件,每天晚上2 30需要对一天前的文件进行

echo“”>>/etc/security/limits.conf echo“*softnproc65535″>>/etc/security/limits.conf echo“*hardnproc65535″>>/etc/security/limits.conf echo“*softnofile65535″>>/etc/security/limits.conf echo“*hardnofile65535″>>/etc/secur…

【自动驾驶】控制算法(八)横向控制Ⅲ | 代码与模型

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

以太网--TCP/IP协议(一)

概述 以太网是局域网的一种&#xff0c;其他的比如还有令牌环、FDDI。和局域网对应的就是广域网&#xff0c;如Internet&#xff0c;城域网等。 从网络层次看&#xff0c;局域网协议主要偏重于低层&#xff08;业内一般把物理层、数据链路层归为低层&#xff09;。以太网协议…

单片机毕业设计基于单片机的智能门禁系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍程序代码部分参考 设计清单具体实现截图参考文献设计获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP…

vue动态统计图的绘画

效果图&#xff1a; 实现&#xff1a; 一、导入依赖 import echarts from echarts 二、vue的代码实现 1.在main.js导入文件 // 引入 echarts 插件 import echarts from echarts // 配置成全局组件 Vue.prototype.$echarts echarts2.代码实现 <template><!--为echa…

韩国火烧车影响出现,浙江出现限制电车进入地下车库,车主难受了

韩国电动汽车起火&#xff0c;烧毁140辆汽车&#xff0c;还导致大楼损坏以及居民受伤的后果&#xff0c;如今在中国市场也产生了影响&#xff0c;《华商报》旗下的《大风新闻》报道指&#xff0c;浙江多地的饭店、大厦禁止电动汽车进入地下车库&#xff0c;这下子电动汽车车主又…

滑动窗口在算法中的应用

滑动窗口是一种经典的算法技巧&#xff0c;就像在处理一系列动态数据时&#xff0c;用一扇可以滑动的“窗口”来捕捉一段连续的子数组或子字符串。通过不断地移动窗口的起点或终点&#xff0c;我们能够以较低的时间复杂度来解决一系列问题。在这篇文章中&#xff0c;我们将通过…