首页 > 编程语言 >Java并发编程实战 07 | 如何正确停止线程

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

时间:2024-09-08 19:54:04浏览次数:13  
标签:Java 07 Thread 中断 System 线程 println out

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

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

  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

标签:Java,07,Thread,中断,System,线程,println,out
From: https://blog.csdn.net/weixin_42627385/article/details/142031576

相关文章

  • Java并发编程实战 08 | 彻底理解Shutdown Hook
    钩子线程(HookThread)简介在一个Java应用程序即将退出时(比如通过正常执行完成或通过用户关闭应用程序),通常需要进行一些清理操作,例如:释放资源(如文件句柄、网络连接)。关闭数据库连接。保存未完成的数据或状态。我们可以通过钩子线程实现这一点,钩子线程是指在程序结束时,JVM......
  • 【JavaScript】LeetCode:16-20
    文章目录16无重复字符的最长字串17找到字符串中所有字母异位词18和为K的子数组19滑动窗口最大值20最小覆盖字串16无重复字符的最长字串滑动窗口+哈希表这里用哈希集合Set()实现。左指针i,右指针j,从头遍历数组,若j指针指向的元素不在set中,则加入该元素,否则更新......
  • JAVA代理-----详细深入介绍
    什么是代理(定义)定义:给目标对象提供一个代理对象,并且由代理对象控制对目标对象的引用为什么要使用JAVA代理(目的)1.功能增强:通过代理业务对原有业务进行增强2.控制访问通过代理对象的方式间接的范文目标对象,防止直接访问目标对象给系统带来不必要的复杂性。例:银行转账的系统......
  • 【Java】Word题库解析2
     初稿见:https://www.cnblogs.com/mindzone/p/18362194一、新增需求在原稿题库之后,还需要生成一份纯题目+ 纯答案答案放在开头,题目里面去掉答案在检查题型时还发现部分内容略有区别: 所以在判断是否为答案的时候需要兼容这种答案二、关于老版本支持doc2000版需要追加......
  • 1-4Java修饰符
    Java修饰符Java语言提供了很多修饰符,主要分为以下两类:访问修饰符非访问修饰符修饰符用来定义类,方法或者变量,通常放在语句的最前端。访问控制修饰符Java中,可以使用访问控制符来保护对类,方法,变量,构造方法的访问。Java支持4种不同的访问权限。default(即默认,什么也不写):在......
  • Day07 字符串part01| LeetCode 344. 反转字符串,541. 反转字符串II,卡码网:54.替换数字
    反转字符串344.反转字符串classSolution{publicvoidreverseString(char[]s){intlens=s.length;intright,left;if(lens%2!=0)//奇数个{right=lens/2+1;left=lens/2-1......
  • Java基础第六天-面向对象编程
    类与对象类就是数据类型,对象就是一个具体的实例。类拥有属性和行为。类是抽象的,概念的,代表一类事物,比如人类,猫类等它是数据类型。对象是具体的,实际的,代表一个具体事物,即是实例。类是对象的模板,对象是类得一个个体,对应一个实例。对象在内存中的存在形式:字符串是指向地址保......
  • 6.跟着狂神学JAVA(数组)
    数组数组是相同类型数据的有序集合每一个数据称作一个数据元素,每个数组元素可以通过一个下标来访问获取数组长度:array.length数组的使用声明数组dataType[]arrayName;初始化数组在声明时初始化int[]numbers=newint[5];//创建一个长度为5的整型数组在声明......
  • 7.跟着狂神学JAVA(面向对象)
    什么是面向对象面向过程步骤清晰简单适合处理一些较为简单的问题线性思维面向对象先分类、然后对分类后的细节进行面向过程的思考适合处理复杂、多人协作的问题分类思维面向对象编程的本质是:以类的方式组织代码,以对象的组织(封装)数据抽象从认识论的角度考......
  • Java毕业设计源码 - ssm框架网上服装销售系统+jsp+vue+数据库mysql+毕业论文等
    文章目录前言一、毕设成果演示(源代码在文末)二、毕设摘要展示1、开发说明2、需求/流程分析3、系统功能结构三、系统实现展示1、用户功能模块2、管理员功能模块四、毕设内容和源代码获取总结逃逸的卡路里博主介绍:✌️码农一枚|毕设布道师,专注于大学生项目实战开发、......