Java并发编程:如何正确停止线程

开发 前端
你可以使用​​interrupt​​方法来通知线程应该中断执行,而被中断的线程拥有决定权,即它不仅可以决定何时响应中断并停止,还可以选择忽略中断。

1. 什么时候需要停止线程?

通常情况下,线程在创建并启动后,会自然运行到结束。但在某些情况下,我们可能需要在运行过程中停止线程,比如:

  • 用户主动取消执行;
  • 线程在运行时发生错误或超时,需要停止;
  • 服务需要立即关闭。

这些情况都需要我们主动停止线程。然而,安全且可靠地停止线程并不容易。Java 语言并没有提供一种机制来确保线程能够立即且正确地停止,但它提供了interrupt方法,这是一种协作机制。

2. 如何正确停止线程?

你可以使用interrupt方法来通知线程应该中断执行,而被中断的线程拥有决定权,即它不仅可以决定何时响应中断并停止,还可以选择忽略中断。

换句话说,如果被停止的线程不想被中断,那么我们除了让它继续运行或强制关闭进程外,别无他法。

3. 为什么不强制停止?而是通知、协作

事实上,大多数时候我们想要停止线程时,至少会让它运行到结束。比如,即使我们在关闭电脑时,也会进行很多收尾工作,结束一些进程并保存一些状态。

线程也是如此。我们想要中断的线程可能并不是由我们启动的,我们对其执行的业务逻辑并不熟悉。如果我们希望它停止,实际上是希望它在停止前完成一系列的保存和交接工作,而不是立即停止。

举个生活中的例子:

某天下午你得知公司要裁员,觉得自己很可能在名单内,便开始找新工作。几周后,成功拿到另一家公司 offer。你准备搬到新公司附近,可家里东西多,只能分批处理。搬到一半时,发现公司裁员结束,自己不在名单中。

你十分高兴,因为喜欢这家公司,决定留下。但一半物品已搬到新家,还得搬回来。

试想,若此时你决定立刻停止搬家、什么都不做,已搬走的物品就会丢失,这无疑是场灾难!

生活中还有很多类似的例子,比如从电脑剪切文件到 U 盘。如果剪切到一半时停止,需要恢复到原来的状态,不能一半文件在 U 盘,一半在电脑上。

4. 代码实践

4.1. 错误的线程停止方式

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

public class StopThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            // 模拟搬家所需时间
            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();
        // 稍后尝试停止
        Thread.sleep(2);
        thread.stop();
    }
}

输出结果(结果可能因计算机性能不同而有所差异,你可以调整时间以获得相同的输出):

Start moving...
1 batches have been moved
2 batches have been moved
3 batches have been moved

可以看到,stop强制线程结束,导致只搬了三批物品,结束后也没有搬回来!

出于安全考虑,stop方法已被官方弃用。你可以在源码中看到它被标记为过时。

@Deprecated
public final void stop() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        checkAccess();
        if (this != Thread.currentThread()) {
            security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
}

4.2. 直接使用interrupt方法,线程并未停止

在主线程中使用interrupt方法中断目标线程,但目标线程并未感知到中断标志,即它不打算处理中断信号。

public class InterruptThreadWithoutFlag implements Runnable {
    @Override
    public void run() {
        System.out.println("Start moving...");
        for (int i = 1; i <= 5; i++) {
            // 模拟搬家所需时间
            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();
        // 稍后
        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中断了这个线程,但它似乎完全忽略了我们的中断信号。就像前面提到的,线程是否停止取决于它自己,因此我们需要修改线程的逻辑,使其能够响应中断,从而停止线程。

4.3. 使用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()) {
                // 做一些收尾工作
                break;
            }
            // 模拟搬家所需时间
            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

从输出结果来看,它与使用stop方法的结果类似,显然线程在执行完之前被停止了,interrupt()方法的中断是有效的,这是一种标准的处理方式。

4.4. 中断某个线程时,线程正在睡眠

如果线程处理中使用了sleep方法,在sleep期间的中断也可以响应,而无需检查中断标志。

例如,使用Thread.sleep(1)模拟每次搬家所需的时间。在主线程中,等待 3ms 后中断,因此预计在搬完 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.getMessage()。

为什么会抛出异常?

这是因为当线程处于sleep状态时,如果接收到中断信号,线程会响应这个中断,而响应中断的方式非常特殊,就是抛出java.lang.InterruptedException: sleep interrupted异常。

因此,当我们的程序中有sleep方法的逻辑,或者可以阻塞线程的方法(如wait、join等),并且可能会被中断时,我们需要注意处理InterruptedException异常。我们可以将其放在catch中,这样当线程进入阻塞过程时,仍然可以响应中断并进行处理。

4.5. 当sleep方法与isInterrupted结合使用时会发生什么?

你注意到在示例 3 的代码中,我们在捕获异常后使用了break来主动结束循环吗?那么,我们是否可以在catch中不使用break,而是在循环入口处判断isInterrupted是否为true呢?

让我们试试:

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()) {
                // 做一些收尾工作
                break;
            }
            // 模拟搬家所需时间
            try {
                Thread.sleep(1);
                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();
        // 稍后
        Thread.sleep(3);
        thread.interrupt();
    }
}

输出(你可能需要调整主线程执行Thread.sleep的时间以获得相同的输出):

Start moving...
1 batches have been moved
2 batches have been moved
sleep interrupted
4 batches have been moved
5 batches have been moved
End of moving

为什么在输出sleep interrupted后,它继续搬了第四和第五批物品?

原因是,一旦sleep()响应了中断,它会重置isInterrupted()方法中的标志,因此在上面的代码中,循环条件检查时,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++) {
            // 模拟搬家所需时间
            Thread.sleep(1);
            System.out.println(i + " batches have been moved");
        }
        System.out.println("End of moving");
    }

    private void goBack() {
        // 做一些收尾工作
    }

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

4.6. 重新中断

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

如前所述,当中断发生并抛出InterruptedException时,isInterrupted的结果会被重置为false。但是,支持再次调用interrupt,这会使isInterrupted的结果变为true。

基于这个前提,我们可以在示例 5 的实现中将run方法改为以下形式:

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

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

4.7 判断中断是否发生的方法

  • 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。

5. JDK 内置的可以响应中断的方法

主要有以下方法可以响应中断并抛出InterruptedException:

  1. Object.wait()/wait(long)/wait(long, int)
  2. Thread.sleep(long)/sleep(long, int)
  3. Thread.join()/join(long)/join(long, int)
  4. java.util.concurrent.BlockingQueue.take()/put(E)
  5. java.util.concurrent.locks.Lock.lockInterruptibly()
  6. java.util.concurrent.CountDownLatch.await
  7. java.util.concurrent.CyclicBarrier.await
  8. java.util.concurrent.Exchanger.exchange(V)
  9. java.nio.channels.InterruptibleChannel的相关方法
  10. java.nio.channels.Selector的相关方法
责任编辑:武晓燕 来源: 程序猿技术充电站
相关推荐

2023-09-08 12:19:01

线程方法interrupt

2022-02-28 07:01:22

线程中断interrupt

2011-12-29 13:31:15

Java

2025-01-10 07:10:00

2024-12-31 09:00:12

Java线程状态

2019-11-07 09:20:29

Java线程操作系统

2023-10-08 09:34:11

Java编程

2025-02-03 08:23:33

2019-09-16 08:45:53

并发编程通信

2022-11-09 09:01:08

并发编程线程池

2023-10-18 15:19:56

2022-03-31 07:52:01

Java多线程并发

2025-02-03 00:40:00

线程组Java并发编程

2024-10-21 18:12:14

2017-09-19 14:53:37

Java并发编程并发代码设计

2023-10-18 09:27:58

Java编程

2010-02-24 10:24:10

Python线程

2017-01-10 13:39:57

Python线程池进程池

2023-09-26 10:30:57

Linux编程

2023-05-12 14:14:00

Java线程中断
点赞
收藏

51CTO技术栈公众号