目录
当点击某个杀毒软件的取消按钮来停止查杀病毒时,或者在控制台敲入quit 命令以结束某个后台服务时……都需要通过一个线程去取消另一个线程正在执行的任务。Java 没有提供一种安全直接的方法来停止某个线程,但是Java提供了中断机制。// 中断线程并不是停止线程
弃用 stop() :
stop() 方法已经被 JDK 废弃,原因就是 stop() 方法太过于暴力,强行把执行到一半的线程终止,存在线程安全问题。
1、Java 线程中断机制原理
Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理(由程序员决定线程如何响应中断)。被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
中断机制是使用一个称为中断状态的内部标志来实现的。调用 Thread.interrupt() 设置此标志,当线程通过调用静态方法 Thread.interrupted() 来检查中断时,中断状态将被清除。非静态的 isInterrupted() 方法被一个线程用来查询另一个线程的中断状态,它不会改变中断标志的状态。// Java 线程中断机制的核心方法
方法 | 方法描述 |
---|---|
public static boolean interrupted() | 判断,静态方法,清除中断标志,重置为 fasle |
public boolean isInterrupted() | 判断,非静态方法,不改变中断标志,通过线程实例调用 |
public void interrupt() | 设置,用来设置中断标志 |
其中, interrupt() 方法是唯一能将中断状态设置为 true 的方法。静态方法 interrupted() 会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。
public class ThreadInterruptTest {
// 计数器
static int i = 0;
public static void main(String[] args) {
System.out.println("begin");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
i++;
System.out.println(i);
// Thread.interrupted() 清除中断标志位
// Thread.currentThread().isInterrupted() 不会清除中断标志位
if (Thread.interrupted()) {
System.out.println("=========");
}
if (i == 5) {
break;
}
}
}
});
t1.start();
// 不会停止线程t1,只会设置一个中断标志位 flag=true
t1.interrupt();
}
}
interrupt() 方法不会停止线程,只会给线程设置一个中断标志位 flag = true。
利用中断机制优雅的停止线程:
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
// 判断中断标记
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(1);
thread.interrupt(); // 设置中断标记为true
}
}
注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况
2、线程 sleep 期间能否感受到中断标志?
修改上面的代码,线程执行任务期间有休眠需求:
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
// 判断中断标记
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
// 增加休眠
try {
Thread.sleep(1); // 抛出异常后,同时清除中断信号
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程停止: stop thread");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(5);
thread.interrupt(); // 主线程设置中断标记为true
}
}
处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样就会导致 while 条件 Thread.currentThread().isInterrupted() 为 false,程序会在不满足 count < 1000 这个条件时退出。如果不在 catch 中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。// 异常清除了中断标志
所以我们需要在 catch 中重新手动添加中断信号,避免线程无法正确停止。
// 增加休眠
try {
Thread.sleep(1); // 抛出异常后,同时清除中断信号
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置线程中断状态为true
Thread.currentThread().interrupt();
}
3、线程中断的使用场景
通常,中断的使用场景有以下几个:
- 点击某个桌面应用中的取消按钮时;
- 某个操作超过了一定的执行时间限制需要中止时;
- 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
- 一组线程中的一个或多个出现错误导致整组都无法继续时;
- 当一个应用或服务需要停止时。
下面来看一个具体的例子。这里新建了一个磁盘文件扫描的任务,扫描某个目录下的所有文件并将文件路径打印到控制台,扫描的过程可能会很长。若需要中止该任务,只需在控制台键入quit并回车即可。// 因为控制台不好输入,代码中设置为 3s 后自动中断
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
public class FileScanner {
private static void listFile(File f) throws InterruptedException {
if (f == null) {
throw new IllegalArgumentException();
}
if (f.isFile()) {
System.out.println(f);
return;
}
File[] allFiles = f.listFiles();
if (Thread.interrupted()) {
throw new InterruptedException("文件扫描任务被中断");
}
if(allFiles != null && allFiles.length > 0){
for (File file : allFiles) {
//还可以将中断检测放到这里
listFile(file);
}
}
}
// 控制台的输入
public static String readFromConsole() {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
return reader.readLine();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
// 3秒后返回"quit",打上中断标记
public static String gainFlagFromTask() {
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "quit";
}
public static void main(String[] args) throws Exception {
final Thread fileIteratorThread = new Thread() {
public void run() {
try {
listFile(new File("C:\\"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread() {
public void run() {
while (true) {
if ("quit".equalsIgnoreCase(gainFlagFromTask())) {
if (fileIteratorThread.isAlive()) {
fileIteratorThread.interrupt();
return;
}
} else {
System.out.println("输入quit退出文件扫描");
}
}
}
}.start();
fileIteratorThread.start();
}
}
在扫描文件的过程中,对于中断的检测这里采用的策略是,如果碰到的是文件就不检测中断,是目录才检测中断,因为文件可能是非常多的,每次遇到文件都检测一次会降低程序执行效率。