赞
踩
线程和任务被创建和启动之后,大部分情况下都是自然运行到结束的,自然停止,但有些情况会需要用到停止线程,如:
用户主动取消
服务被快速关闭
运行出错或超时情况下等线程都需要被停止
这些情况都需要主动来停止线程,想让线程安全可靠停止下来并不容易,Java语言没有一种机制来安全正确地停止线程,但是它提供了interrupt,这是一种协作机制。
可以使用interrupt
来通知中断,但它不是强制停止。
通俗理解:就是用一个线程来通知另一个线程让它停止工作,在Java中,如果想停止一个线程,能做的最多能做的就是告诉一个线程,你该中断了,而被中断的线程本身拥有决定权,它不仅能决定何时去响应这个中断,何时去停止,还拥有最高决定权,就是停不停止,也就是说,如果被停止线程不想被中断,那我们对此无能为力,根本没有能力做到强行停止。但是在开发过程中,开发的各个部门和小组都遵守良好的规范的话,是可以都把代码处理成可以响应interrupt
中断来停止的,但这仅仅是一个规范,不是一种强制。
其实大多数时候我们想停止一个线程,都会至少让它运行到结束,比如说,即便我们关机的时候,也会在关机的时候做很多的收尾工作,结束一些进程、线程,保存一些状态,那么线程也是一样的,由于我们想中断的线程,可能不是我们开发的,对这个线程执行的业务逻辑根本就不熟悉,如果想让它停止,其实是希望它完成一系列的保存工作,交接工作,再停止,而不是立刻停止,让它陷入一种混乱的状态。所以,被停止的那个线程,对自己的业务逻辑是最熟悉的,而发出停止信号的线程,它对别人的业务逻辑很可能是不了解的,所以java语言设计的时候就把线程停止的权力和步骤交给了被停止线程本身,这就是停止线程的核心,而不是强制停止。
/** * 描述: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 的中断,这样便能停止线程。
!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()方法中断有效。
代码演示:
/** * 描述:带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
中处理,这样在线程进入阻塞过程中,依然可以响应这个中断并进行处理。
代码演示:
/** * 描述:带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()
判断检查是否已中断。
/** * 描述:如果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
后续检查过程中检查不到任何被中断过的迹象,导致程序不能退出。
如何解决呢?下面有两种方案。
有时候在run()
方法中不会一次性把所有业务都写在该方法中,我们可能会调用其他子方法。假设被调用的子方法在某些代码环节可能需要处理InterruptedException
异常,这时通常有两种方法:一是try/catch
,二是在方法签名上直接抛出这个异常 throws xxxException
。
下面先来演示一下非常不好的try/catch
,并说一下为什么不好。
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()
方法的小伙伴的责任绝不是把异常简单的打印出来,自己吞掉,应该上报给我们,把中断的这个信息传给我们。
目的是让上层方法能感知到这个异常并做出相应的处理!!!
/** * 描述: 最佳实践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
这个顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。
我们上面说优先处理中断的方法是传递中断,但是在有些情况我们是无法传递的,比如说,我们是作为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等;
用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
会导致原来的逻辑没完整做完就停止了。
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来中断线程是可行的。
代码说明:下面的例子,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
// 主类: 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
来停止线程。用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
,程序正常中断了。
注意 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。
使用interrupt方法请求中断,而不是强制终止线程,是合作机制。这样,被请求中断的线程可以自主决定,处理自己的逻辑。好处是,可以保证数据安全,来得及清理,能够保证数据完整性。
请求方发出中断请求
被停止方要在每次循环中或适当的时候检查中断信号,并在可能抛出InterruptException
的时候处理这个信号
如果线程中响应中断的是子方法,子方法被外层方法调用,有两种响应中断的最佳方案:
stop会突然停止线程,线程来不及处理剩下的数据,会导致数据不完整
suspend等方法会使线程挂起,不会破坏对象,抱着锁阻塞,会导致死锁
用volatile设置boolean标记位无法处理长时间阻塞的情况,导致线程无法停止
如果线程阻塞是由于调用了wait(),sleep()或join()方法,你可以中断线程,通过抛出 InterruptedException异常来唤醒该线程。
需要根据不同的类调用不同的方法。
如果线程阻塞是由于调用了 wait(),sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来从阻塞中唤醒该线程。
但是对于不能响应InterruptedException的阻塞,很遗憾,并没有一个通用的解决方案。
但是我们可以利用特定的其它的可以响应中断的方法,比如ReentrantLock.lockInterruptibly(),比如关闭套接字使线程立即返回等方法来达到目的。
答案有很多种,因为有很多原因会造成线程阻塞,所以针对不同情况,唤起的方法也不同。
总结就是说如果不支持响应中断,就要用特定方法来唤起,没有万能药。
文章来源:深度解析线程的正确停止方法
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。