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

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

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

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

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

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

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

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

因此为了避免上述问题,我们需要一种温和的方法来停止线程,确保它能够完成必要的收尾工作,以避免出现数据不完整或数据不一致的状态。

如何正确停止一个线程?

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

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

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

代码实践

使用stop()停止线程

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

java 复制代码
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 方法来中断目标线程时,目标线程可能无法感知到中断标志,也就是说,即使主线程发出了中断请求,目标线程可能继续运行,不会及时停止或做出其他响应。这种情况可能会导致线程无法按照预期停止,从而影响系统的稳定性和性能。

java 复制代码
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,则表示线程已经收到中断信号,线程可以据此执行相应的中断处理逻辑。

java 复制代码
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 批物品后线程会被中断,代码如下:

java 复制代码
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()判断?这样看起来更自然一些。让我们尝试一下:

java 复制代码
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这里直接处理这个中断异常,而是直接将异常向上抛出,交由更高层次处理,便于功能分解和团队协作。你可以把把中断的后续收尾处理封装成一个方法,如下:

java 复制代码
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 方法可以改为如下形式:

java 复制代码
@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() 方法针对的是当前线程,从源代码中可以很容易地看到:

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

java 复制代码
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:

java 复制代码
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
相关推荐
luoluoal4 分钟前
基于Spring Boot的装饰工程管理系统源码(springboot)
java·spring boot·后端
J不A秃V头A19 分钟前
IDEA实用小技巧:方法之间的优雅分割线
java·intellij-idea
涛涛6号30 分钟前
PageHelper(springboot,mybatis)
java·spring boot·后端
夜雨翦春韭41 分钟前
【代码随想录Day58】图论Part09
java·开发语言·数据结构·算法·leetcode·图论
豪宇刘1 小时前
Shiro回话管理和加密
java·后端·spring
V+zmm101341 小时前
警务辅助人员管理系统小程序ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·课程设计·1024程序员节
Seven 7 Chihiro1 小时前
[进阶]java基础之集合(三)数据结构
java·开发语言
小爬虫程序猿2 小时前
Java爬虫的京东“寻宝记”:揭秘商品类目信息
java·开发语言
耀耀_很无聊2 小时前
第十一部分 Java 数据结构及集合
java·开发语言·数据结构
webfunny20202 小时前
IDEA集成AI的DevAssist插件使用指南
java·ide·intellij-idea