首先需要强调的是中断线程只有一种方法,就是Thread类的interrupt方法。
接下来通过代码演示两种最佳中断线程的方式,以及关于中断线程需要注意的点,请仔细体会每一个示例代码。
先来看一个最基本的中断线程的情况(子线程中无sleep方法):
public class RightWayStopThreadWithoutSleep implements Runnable {
@Override
public void run() {
int num = 0;
while (num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
执行这段代码会发现,thread.interrupt()这行代码好像并不起作用,子线程还是输出了所有的结果(我们想要的结果是子线程只能输出1秒,然后发生中断)。子线程并没有理会我们的中断请求。这里要强调的是使用interrupt来通知,而不是强制线程停止,它是否响应中断完全取决操作系统。
因为在HotSpot虚拟机中,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,所以HotSpot自己是不会去干涉线程调度的,全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的。
回到上面的代码,想要达到我们的目的,应该要这样写(只展示核心部分):
@Override
public void run() {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
通过Thread.currentThread().isInterrupted()判断线程是否处于中断状态,通过逻辑判断决定是否继续执行while循环。
再来看个子线程中带有sleep方法的中断例子:
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 9999999) {
if (num % 10 == 0) {
System.out.println(num + "是10的倍数");
}
num++;
}
//思考点
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(100);
thread.interrupt();
}
}
这里的运行结果也是和上面一样并没有理会我们的中断请求,需要解释下的是:主线程休眠了100毫秒再对子线程进行中断,此时子线程还在执行while循环,并没有执行到Thread.sleep(10)这一行代码,所以线程也没有理会我们的中断请求。所以要想达到我们的效果,应该和上面一样,在while循环的时候加上Thread.currentThread().isInterrupted()的判断。
一.最佳之传递中断
直接看代码:
public class RightWayStopThreadInProd implements Runnable {
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
//恢复中断的标记位,这里暂时不用管,下面会解释。
Thread.currentThread().interrupt();
//保存日志、停止程序
System.out.println("保存日志");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
//这里代表主线程停顿1秒,让子线程能打印一会。
Thread.sleep(1000);
//中断子线程
thread.interrupt();
}
}
如果throwInMethod方法不想处理异常,应该在方法签名中抛出异常,交给调用者去处理。因为往上抛到最顶层,也就是run()就会强制try/catch,run方法是不能抛异常的。
二.最佳之恢复中断
public class RightWayStopThreadInProd2 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted,程序运行结束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断。因为sleep()方法响应中断请求后或者说抛出异常后,会清除中断标志位,所以要恢复中断。
两种方法介绍完了,所以我们不应该在开发中不理会中断,也就是catch InterruptedException后什么也不做,这是不好的实现。
三.易出错的点
3.1 如果在执行过程中,每次循环都会调用sleep或wait等方法,那么不需要每次迭代都检查是否已中断
先看代码:
public class RightWayStopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
//思考为什么没有和上面一样调用Thread.currentThread().interrupt()进行判断?
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(3000);
thread.interrupt();
}
}
思考一下在while循环的条件上为什么没有和上面一样调用Thread.currentThread().interrupt()进行判断?其实,上面已经解释过类似的情况了。因为主线程休眠了3秒再对子线程进行中断,而子线程中的这段代码执行速度是非常快的:
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
所以在接收到中断信号时,while循环一定会执行在Thread.sleep(10)这行代码中进行等待,那么就能及时响应中断,抛出异常,中止子线程,就不需要每次循环都判断Thread.currentThread().interrupt()了。
3.2 如果while里面放try/catch,会导致中断失效
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
这里抛出异常后,程序依然正常运行,原因上面也解释过了,抛出异常后,会清除中断标志位,所以正确的做法是要在catch中恢复中断。
四.能响应中断的常用方法
五.停止线程相关的重要函数解析
5.1 判断线程是否已被中断
Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是如果线程处于中断状态,调用一次返回线程中断状态为true,再调用一次会使得这个线程的中断状态重新转为false;
isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。
这里的isInterrupted()是实例方法,调用它需要通过线程对象,interrupted()是静态方法,实际会调用Thread.currentThread()操作当前线程。
来看一道练习题,可以更好的掌握概念:
public class RightWayInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
}
}
});
// 启动线程
threadOne.start();
//设置中断标志
threadOne.interrupt();
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
//获取中断标志并重置
System.out.println("isInterrupted: " + threadOne.interrupted());
//获取中断标志并重直
System.out.println("isInterrupted: " + Thread.interrupted());
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
threadOne.join();
System.out.println("Main thread is over.");
}
}
输出如下:
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象。也就是对应上面的main线程,main线程并没有中断,所以2和3都输出false。
5.2 interrupt方法原理
interrupt的设计非常强大,能够让线程处于等待状态还能响应,那这是怎么做到的呢?我们来看看它的实现原理。interrupt里调用了interrupt0方法,是native方法,追溯到hotspot中的c++代码中:
可以看到线程状态是在这里被设置的,然后对一些情况进行唤醒,比如_SleepEvent。SleepEvent就是对应Thread.sleep方法,((JavaThread*)thread)->parker()就是对应LockSupport.park方法,ParkEvent就是对应synchronized同步块及Object.wait方法。
参考书籍: 《深入理解Java虚拟机第三版》