深度解析线程的正确停止方法
一、解惑
1. 什么情况下,线程需要被停止?
线程和任务被创建和启动之后,大部分情况下都是自然运行到结束的,自然停止,但有些情况会需要用到停止线程,如:
- 用户主动取消
- 服务被快速关闭
- 运行出错或超时情况下等线程都需要被停止
这些情况都需要主动来停止线程,想让线程安全可靠停止下来并不容易,Java语言没有一种机制来安全正确地停止线程,但是它提供了interrupt,这是一种协作机制。
2. 如何正确停止线程?
可以使用interrupt
来通知中断,但它不是强制停止。
通俗理解:就是用一个线程来通知另一个线程让它停止工作,在Java中,如果想停止一个线程,能做的最多能做的就是告诉一个线程,你该中断了,而被中断的线程本身拥有决定权,它不仅能决定何时去响应这个中断,何时去停止,还拥有最高决定权,就是停不停止,也就是说,如果被停止线程不想被中断,那我们对此无能为力,根本没有能力做到强行停止。但是在开发过程中,开发的各个部门和小组都遵守良好的规范的话,是可以都把代码处理成可以响应interrupt
中断来停止的,但这仅仅是一个规范,不是一种强制。
3. 我们是程序的控制者,凭什么我们没有控制线程停止的权利?
其实大多数时候我们想停止一个线程,都会至少让它运行到结束,比如说,即便我们关机的时候,也会在关机的时候做很多的收尾工作,结束一些进程、线程,保存一些状态,那么线程也是一样的,由于我们想中断的线程,可能不是我们开发的,对这个线程执行的业务逻辑根本就不熟悉,如果想让它停止,其实是希望它完成一系列的保存工作,交接工作,再停止,而不是立刻停止,让它陷入一种混乱的状态。所以,被停止的那个线程,对自己的业务逻辑是最熟悉的,而发出停止信号的线程,它对别人的业务逻辑很可能是不了解的,所以java语言设计的时候就把线程停止的权力和步骤交给了被停止线程本身,这就是停止线程的核心,而不是强制停止。
二. 停止线程的实践
1. 普通情况的线程中断
(1)直接使用 interrupt 方法,线程未停止
/**
* 描述:run方法没有 sleep 和 wait方法
* */
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();
}
}
打印结果:
你会发现,没有任何效果,我们使用 interrupt 想把这个线程中断,但它似乎根本就没理会我们,这个线程想不想停止,取决于它本身,所以我们需要对它进行改变,去响应 interrupt 的中断,这样便能停止线程。
(2)增加中断响应 !Thread.currentThread().isInterrupted()
。
当线程被中断时,即Thread.currentThread().isInterrupted()
等于 true
时,!Thread.currentThread().isInterrupted()
等于false
,线程被停止。
/**
* 描述:run方法没有 sleep 和 wait方法
* */
public class RightWayStopThreadWithOutSleep implements Runnable{
@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("任务运行结束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithOutSleep());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果:
看打印结果中最后的数值与(1)中的打印结果相比,明显线程未执行完就被停止了,interrupt()方法中断有效。
2. 阻塞情况下的线程中断
代码演示:
/**
* 描述:带sleep的中断线程方法
* */
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
try {
while (!Thread.currentThread().isInterrupted() && num < 300){
if (num % 100 == 0){
System.out.println(num+"是能被100整除的数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt(); // 睡眠 500 毫秒后执行 interrupt,线程应在执行到了 Thread.sleep(1000);这个阶段
}
}
打印结果:
发现报异常,回顾一下,当我们在写sleep()
方法时,代码要求我们try-catch
这个异常,然后打印的结果也是catch
到了,即java.lang.InterruptedException: sleep interrupted
,为什么会报异常呢,是这样的,当线程在休眠状态下,如果收到这个中断信号,线程便会响应这个中断,而响应这个中断的方式非常特殊,就是抛出这个异常,于是我们就在 catch
中打印了这个异常sleep interrupted
,就是在sleep
过程中被打断了。
所以当我们程序带有sleep
,或者能让线程阻塞的方法,并且有可能被中断的时候,需要注意处理 InterruptedException
这个异常,我们可以放在 catch
中处理,这样在线程进入阻塞过程中,依然可以响应这个中断并进行处理。
3. 线程每次迭代都阻塞
代码演示:
/**
* 描述:带sleep的中断线程方法
* */
public class RightWayStopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
try {
while (num <= 10000){
if (num % 100 == 0){
System.out.println(num+"是能被100整除的数");
}
num++;
Thread.sleep(10); // slepp方法放在循环里
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
当线程的阻塞方法是在循环中时,就不再需要 Thread.currentThread().isInterrupted()
判断线程是否被中断了,这是因为在整个循环过程中,代码的大部分运行时间都是消耗在了Thread.sleep(10)
中的,所以极有可能是在sleep
时接收到interrupt
,自然会抛出InterruptedException
异常,不需要在代码中加入 Thread.currentThread().isInterrupted()
判断检查是否已中断。
4. 基于第3步的代码:如果While里面放 try / catch,会导致中断失效
/**
* 描述:如果While里面放try / catch,会导致中断失效
* */
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
while (num <= 10000){
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();
}
}
打印结果:
我们发现,执行 interrupt
之后,明明已经catch
了InterruptedException
异常,但在线程并没有停止,反而继续执行,这是为什么呢?因为抛出异常后被catch
住了,但是循环并没有跳出,不满足跳出循环的条件,会继续执行while循环。
那是不是加上在while()里加上&& !Thread.currentThread().isInterrupted()
,在线程中断后,下一次循环开始时判断一下线程是否已被中断就可以了呢?来试一试吧。
/**
* 描述:如果While里面放try / catch,会导致中断失效
* */
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
//while()加一个条件:!Thread.currentThread().isInterrupted()
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();
}
}
线程依然没有停止!!!
原因是java
在设计sleep()
函数的时候,有这样一个理念,当它一旦响应中断,于是便会把isInterrupted()
这个标记位给清除,所以在上面代码while
后续检查过程中检查不到任何被中断过的迹象,导致程序不能退出。
如何解决呢?下面有两种方案。
4.1 传递中断
有时候在run()
方法中不会一次性把所有业务都写在该方法中,我们可能会调用其他子方法。假设被调用的子方法在某些代码环节可能需要处理InterruptedException
异常,这时通常有两种方法:一是try/catch
,二是在方法签名上直接抛出这个异常 throws xxxException
。
下面先来演示一下非常不好的try/catch
,并说一下为什么不好。
4.1.1 举一个使用try-catch
的例子:
代码过程是:调用一个通过try/catch
处理异常的方法,方法内容是睡眠2秒,启动子线程后,主线程睡眠 1秒后发起中断请求,确保子线程是在throwInMethod
方法里的sleep
时响应中断。
/**
* 描述: 最佳实践:catch了interruptedException之后的优先选择:在方法签名中抛出异常
* 那么在run()方法就会强制要求try/catch
* */
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("go");
throwInMethod();
}
}
private void throwInMethod() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new
RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果:
try/catch不好的原因如下
上图明显异常已经打印了,但是不好的是,如果线程继续运行,在茫茫控制台信息中是很难注意到这个异常的,这就导致很难去处理它,所以实际上当有一个线程来打断我们时,我们没有处理好,而且把这个异常给忽略掉了,在实际生产环境中,我们就不一定能感知到这个异常。假设throwInMethod()
方法是其他人员写的,我们只是负责调用的话,那遗憾的事情就发生了,别人想中断我们,但我们没有响应,而且可能还毫不知情,因为我们是负责写run()
方法的内容,throwInMethod()
方法是其他小伙伴负责的,我们并不了解里面的业务逻辑,只是简简单单地调用它,最后的责任却是在我们这里,因为其他线程想中断我们,我们却没有响应中断。
在throwInMethod()
方法中直接把中断给吞了,什么叫吞了呢?就是在throwInMethod()
方法休眠的过程中有一个中断过来,但是它没有做什么处理,只是把它打印出来,没有上报给调用它的方法(run方法),它要做的应该是上报给我们run方法,因为它无法做出更多中断处理,实际应交给我们调用方,交给run
方法处理,去决定在调用方法这步代码有异常情况我们应该怎么处理,是该保存日志或其它操作等等,这是我们的责任,而编写throwInMethod()
方法的小伙伴的责任绝不是把异常简单的打印出来,自己吞掉,应该上报给我们,把中断的这个信息传给我们。
4.1.2 方法签名上抛出这个异常 throws InterruptedException
目的是让上层方法能感知到这个异常并做出相应的处理!!!
/**
* 描述: 最佳实践1:catch了interruptedException之后的优先选择:在方法签名中抛出异常
* 那么在run()方法就会强制要求try/catch
* */
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
//保存日志、停止程序
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();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果:
小结: 在被调用的字方法里如果有InterruptedException
异常时,优先选择在方法上抛出异常,即传递中断:。用throws InterruptedException
标记你的方法,不采用try
语句块捕获异常,以便于该异常可以传递到顶层,让run
方法可以捕获这一异常,例如:
void subTask() throws InterruptedException{
sleep(delay);
}
由于run
方法内无法抛出checked Exception
,即只能用try catch
,所以run
这个顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。
4.1.2 不想传递或者无法传递中断:那就选择恢复中断
我们上面说优先处理中断的方法是传递中断,但是在有些情况我们是无法传递的,比如说,我们是作为run()
方法的编写者,在run()
方法中是不允许抛出异常的,或者有时候我们确实不想在这个方法上抛出异常,就是要自己处理的话,这边也给出了一种对应的方法:恢复中断。
恢复中断总体而言就是我们在获取InterruptedException
的同时,应该在catch
语句中再次调用Thread.currentThread().interrupt()
,这样就相当于自己把中断重新设置了一遍,这样一来在后续的执行中依然能检测到刚才发生的这个中断,并且有后续的逻辑继续去处理。
代码演示:
/**
* 描述: 最佳实践2:在catch子语句中调用Thread.currentThread.interrupt()来恢复中断状态,
* 以便于在后续的执行中,依然能够检查到刚才发生的中断
* 回到刚才RightWayStopThreadInProd补上中断,让它跳出
* */
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();
}
}
打印结果:
所以:如果不想或无法传递InterruptedException
(例如用run方法的时候,就不让该方法throws InterruptedException
),那么应该选择在catch
子句中调用Thread.currentThread().interrupt()
来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断,正常退出。
可以响应中断的方法总结列表:
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的相关方法
unchecked Exception 和 checked Exception的概念:
java 异常体系 Throwable 分为两类:Error和Exception。
Error是代码层面无法处理的系统之类的问题;Exception分为 RuntimeException、IOException等,RuntimeException 和 Error 属于 unchecked Exception(不受检查异常),因为编译器无法对这类异常提前预测。而Exception中除了 RuntimeException 以外的其他 Exception 都属于 checked Exception(受检查异常),因为可以被编译器提前预知,并对可能出现的异常执行对应的代码处理,比如 try/catch、throws等;
三、错误的停止方法
1. 被弃用的stop,suspend和resume方法
用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)。
public class StopThread implements Runnable {
@Override
public void run() {
//模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队" + i + "开始领取武器");
for (int j = 0; j < 10; j++) {
System.out.println(j);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队"+i+"已经领取完毕");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new StopThread());
thread.start();
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop();
}
}
打印结果:
上面可以看到,stop太强制,连队1的人员还没有领取完就停止了,也就是说stop
会导致原来的逻辑没完整做完就停止了。
2. 用volatile设置boolean标记位
(1) 部分场景可行
public class WrongWayVolatile implements Runnable {
private volatile boolean canceled = false;
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
System.out.println(num + "是能被100整除的数");
}
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
WrongWayVolatile r = new WrongWayVolatile();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(5000);
r.canceled = true;
}
}
打印结果:
这种情况下通过volitile来中断线程是可行的。
(2) 不可行的场景
代码说明:下面的例子,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
// 主类:
public class WrongWayVolatileCantStop {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take()+"被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");
//一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况生产者还是会处于阻塞
producer.canceled=true;
System.out.println(producer.canceled);
}
}
// 生产者:
class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
// 消费者:
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
打印结果:
我们发现volatile
设置为了true
,但是生产者并没有停下来。
为什么说用volatile停止线程不够全面?
- 这种做法是错误的,或者说是不够全面的,在某些情况下虽然可用,但是某些情况下有严重问题。
- 此方法错误的原因在于,如果我们遇到了线程长时间阻塞(这是一种很常见的情况,例如生产者消费者模式中就存在这样的情况),就没办法及时唤醒它,或者永远都无法唤醒该线程,而
interrupt
设计之初就是把wait
等长期阻塞作为一种特殊情况考虑在内了,我们应该用interrupt
来停止线程。
(3) 改进(2)中的代码
用interrupt
中断来修复刚才的无尽等待问题
// main函数:
public class WrongWayVolatileFixed {
public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixed body = new WrongWayVolatileFixed();
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = body.new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(storage);
while (consumer.needMoreNums()) {
System.out.println(consumer.storage.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了。");
producerThread.interrupt();
System.out.println(producer.isCanceled);
}
// 生产者:
class Producer implements Runnable {
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了。");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}
// 消费者:
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
}
打印结果:
使用interrupt
,程序正常中断了。
(4) 判断是否已被中断的相关方法
- static boolean interrupted() 判断当前线程是否被中断,调用后会把中断线程的标记直接设为false,即清除中断标记
- boolean isInterrupted() 判断当前线程是否被中断,不会清除中断标记
注意 static boolean interrupted() 方法的目标对象是“当前线程”,而不管本方法来自于哪个实例对象:
public class RightWayInterrupted {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(() -> {
for (; ; ) {
}
});
// 启动线程
threadOne.start();
//设置中断标志
threadOne.interrupt();
//获取中断标志
System.out.println("isInterrupted: " + threadOne.isInterrupted());
//获取中断标志并重置:threadOne.interrupted()虽然是由threadOne线程发出的,但是实际执行的对象还是主线程
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.");
}
}
打印结果:
interrupted()
是静态方法,无论是对象调用还是类调用判断的都是主线程的中断标志,所以应该都是 false。
四、总结
1. 停止线程使用 interrupt 方法
使用interrupt方法请求中断,而不是强制终止线程,是合作机制。这样,被请求中断的线程可以自主决定,处理自己的逻辑。好处是,可以保证数据安全,来得及清理,能够保证数据完整性。
2. 若让interrupt方法起效,需要多方面的配合使用
- 请求方发出中断请求
- 被停止方要在每次循环中或适当的时候检查中断信号,并在可能抛出
InterruptException
的时候处理这个信号 - 如果线程中响应中断的是子方法,子方法被外层方法调用,有两种响应中断的最佳方案:
- 1)传递中断、即优先在子方法层向上抛出异常,将中断信号传给run方法,在run方法层处理中断信号逻辑
- 2)恢复中断、即子方法收到中断信号后,再次设置中断状态。
3. 如果不用interrupt,其他方法会有一定的弊端与后果
- stop会突然停止线程,线程来不及处理剩下的数据,会导致数据不完整
- suspend等方法会使线程挂起,不会破坏对象,抱着锁阻塞,会导致死锁
- 用volatile设置boolean标记位无法处理长时间阻塞的情况,导致线程无法停止
如果线程阻塞是由于调用了wait(),sleep()或join()方法,你可以中断线程,通过抛出 InterruptedException异常来唤醒该线程。
4. 无法响应中断时如何停止线程,即如何处理不可中断的阻塞
需要根据不同的类调用不同的方法。
如果线程阻塞是由于调用了 wait(),sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来从阻塞中唤醒该线程。
但是对于不能响应InterruptedException的阻塞,很遗憾,并没有一个通用的解决方案。
但是我们可以利用特定的其它的可以响应中断的方法,比如ReentrantLock.lockInterruptibly(),比如关闭套接字使线程立即返回等方法来达到目的。
答案有很多种,因为有很多原因会造成线程阻塞,所以针对不同情况,唤起的方法也不同。
总结就是说如果不支持响应中断,就要用特定方法来唤起,没有万能药。