什么时候需要停止一个线程?
一个线程被创建并启动之后,大部分情况下都会自然运行至结束,但是也有一些情况需要主动停止线程,比如:
- 用户主动取消执行:用户可能会中止一个正在进行的操作,这时需要停止相关线程。
- 运行时错误或超时:线程可能因为运行时错误或超时而需要被停止,以避免长时间占用资源。
- 服务关闭:当服务即将关闭时,可能需要停止所有正在运行的线程,以释放资源并确保干净地关闭服务。
所有这些情况都需要主动停止线程,但是要安全可靠地停止线程并不是一件容易的事情。
为什么不能强制停止线程?
实际上,当我们停止一个线程时,通常希望它至少能完成一些必要的收尾工作,如保存数据、切换状态等,而不是立即停止,以免导致状态混乱。
生活中,我们经常会遇到类似的情况。例如,当你将文件从电脑剪切并粘贴到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