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
:
-
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)
-
java.nio.channels.InterruptibleChannel
的相关方法 -
java.nio.channels.Selector
的相关方法
好了,这次的内容就到这里,下次再见!
原文始发于微信公众号(程序猿技术充电站):Java并发编程:如何正确停止线程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/310469.html