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

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

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

  • 用户主动取消执行;

  • 线程在运行时发生错误或超时,需要停止;

  • 服务需要立即关闭。

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

2. 如何正确停止线程?

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

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

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

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

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

举个生活中的例子:

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

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

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

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

4. 代码实践

4.1. 错误的线程停止方式

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

java 复制代码
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();
    }
}

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

sql 复制代码
Start moving...
1 batches have been moved
2 batches have been moved
3 batches have been moved

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

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

java 复制代码
@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方法中断目标线程,但目标线程并未感知到中断标志,即它不打算处理中断信号。

java 复制代码
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();
    }
}

输出:

sql 复制代码
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,可以根据此进行中断后的处理逻辑。

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()) {
                // 做一些收尾工作
                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();
    }
}

输出(结果可能不一致):

sql 复制代码
Start moving...
1 batches have been moved
End of moving

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

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

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

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

输出:

sql 复制代码
Start moving...
1 batches have been moved
2 batches have been moved
sleep interrupted
End of moving

发现了吗?额外输出了sleep interrupted。这是因为发生了中断异常,我们在catchInterruptedException后输出了e.getMessage()

为什么会抛出异常?

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

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

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

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

让我们试试:

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()) {
                // 做一些收尾工作
                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的时间以获得相同的输出):

sql 复制代码
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处理中断异常,而是直接将异常向上抛出,由调用方处理。

可以将当前逻辑封装到一个单独的方法中,并将中断后的收尾处理也封装到另一个方法中,如下所示:

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++) {
            // 模拟搬家所需时间
            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方法改为以下形式:

java 复制代码
@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()方法的目标是当前线程,无论该方法是从哪个实例对象调用的,从源码中可以很容易看出:

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());
    }
}

输出:

vbnet 复制代码
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的相关方法

好了,这次的内容就到这里,下次再见!

相关推荐
用户79034903371几秒前
springboot集成redisson实现redis分布式锁
后端
陈随易6 分钟前
程序员的新玩具,MoonBit(月兔)编程语言科普
前端·后端·程序员
码出极致12 分钟前
Redisson秒杀系统中的分布式锁应用
后端
Shimiy18 分钟前
第四章 数组
java
间彧18 分钟前
什么是JVM Young GC
java·jvm
xiaok19 分钟前
@Param注解的作用
java·后端
脑袋大大的22 分钟前
钉钉企业应用开发技巧:查询表单实例数据新版SDK指南
java·钉钉·企业应用开发
Sperains27 分钟前
async/await和Synchronous的区别
后端
码出极致30 分钟前
Redisson可重入锁(RLock)的使用与原理
后端
vx_bscxy32233 分钟前
springboot排课系统 -计算机毕业设计源码-23791
spring boot·后端·课程设计