前言
上一篇博客简单介绍了线程启动的方式,这一篇博客打算介绍一下如何停止线程,Java中停止线程相对来说就比较麻烦了,如何正确的停止线程其实也是一个比较常见的面试考题,需要详细总结一下。
线程停止的原理
Java中线程的停止并不是像关闭一个开关一样,直接停止线程,Java中的线程停止原理有点类似于计算机组成原理中对中断的处理,首先关闭中断标志位,让后响应中断,然后处理中断。
Java中线程的处理也可以看成大致的这个过程——**线程本身响应外部的中断通知,然后将中断标志位复位,但是什么之后线程停止,由线程本身自己决定。**这也是Java中最好的停止线程的方式。
如何正确停止线程
其实不同状态的线程,对中断信号的响应是不同的,因此在不同的线程逻辑的停止方式也有细微的差异。这里简单总结一下。
正常运行状态的线程停止
实例代码
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 + " is 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();
}
}
正常的运行结果如下:
sleep状态的线程停止
如果目标线程本身是在sleep的阻塞状态,这个时候目标线程响应中断的方式也是有些许差异的
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
while(num<=300 && !Thread.currentThread().isInterrupted()){
if(num %100 ==0){
System.out.println(num + " is 100的倍数");
}
num++;
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
运行结果
在sleep阻塞状态中的线程,响应中断的方式比较特殊,是直接抛出异常。 这也是Java为什么强制需要我们对sleep抛出的异常进行处理的原因。目标线程在sleep的时候,依旧能响应中断。
需要说明一下的是,上面的实例中,每次循环的时候,在while的时候,都判断了一下中断的标志位,如果sleep在while循环体中(也就是每次循环sleep一下),这种情况下while循环的开头,是不需要判断中断标志位的
public class RightWayStopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
try {
int num = 0;
while (num <= 10000 ) {
if (num % 100 == 0) {
System.out.println(num + " is 100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
还有一个需要注意的是,sleep响应中断之后,中断标志位会复位(这一点和计算机组成原理中的中断响应太像了),将上述代码中while循环外的try-catch放入到循环体内部,即可看到不同的效果
public class CanInterrupt {
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 + " is 100 的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
运行结果如以下动图所示:
在响应中断之后,目标线程并没有终止,而是继续欢快的运行,所以在停止sleep阻塞状态中的线程的时候,需要注意中断标志位的复位问题。
较好的停止线程的方式
针对sleep本身对中断标志位清除的问题,简单总结了一下正确停止线程的方法。
1、子方法的中断异常需要抛出
先说问题,如果将中断的异常,在子方法中进行处理,则相关异常在处理完成之后,中断标志位复位。并不能完成中断的
public class ProdRightWayStopThread implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("running");
catchInterruptInMethod();
}
}
private void catchInterruptInMethod() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ProdRightWayStopThread());
thread.start();
Thread.sleep(900);
thread.interrupt();
}
}
运行结果和上面的代码参不多,一直running,输出一行异常信息之后,继续running,如果这是生产,则相关异常的日志会淹没在海量的日志数据中……
因此较好的方式,是强制在run方法中处理子方法的相关异常(不只是InterruptedException)
public class ProdRightWayStopThread implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("running");
try {
catchInterruptInMethod();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
}
}
private void catchInterruptInMethod() throws InterruptedException {
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ProdRightWayStopThread());
thread.start();
Thread.sleep(900);
thread.interrupt();
}
}
2、恢复中断标志位
如果子方法确实无法抛出中断的相关异常,可以通过恢复中断标志位来进行,还是基于上述的实例进行优化
@Slf4j
public class ProdRightWayStopThreadResetInterrupt implements Runnable {
@Override
public void run() {
while (true) {
if(Thread.currentThread().isInterrupted()){
log.info("thread is interrupted,线程中断,运行终止");
break;
}
System.out.println("running");
catchInterruptInMethod();
}
}
private void catchInterruptInMethod() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("收到中断信号,处理中断,信息为:{}",e);
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ProdRightWayStopThreadResetInterrupt());
thread.start();
Thread.sleep(10000);
thread.interrupt();
}
}
总的来说线程停止较好的方式就两种,无非就是将中断异常抛出,或者重新开中断。
几种错误的停止方式
1、stop,suspend和resume
这几种方式其实已经被弃用了,自然是错误的,相比之前介绍的几种比较优雅的方式,这种方式暴力的多,并且会造成脏数据,这些脏数据会对程序的后续运行产生很大的问题,这个就不做具体的实例了。suspend是带着锁进入到阻塞,容易造成死锁,因此被丢弃
2、volatile修饰的标记位
关于采用volatile修饰的变量,来停止线程,这个在有些博客或者书籍上归纳为正确的停止方式,但是在目标线程阻塞的时候,这种方式并不能正确停止线程。
@Slf4j
public class WrongWayVolatileCannotStop{
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue dataQueue = new ArrayBlockingQueue(10);
Producer producer = new Producer(dataQueue);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(10000);
Consumer consumer = new Consumer(dataQueue);
while(consumer.canConsumer(dataQueue)){
log.info("数据:{} 被消费者消费了",consumer.dataQueue.take());
Thread.sleep(100);
}
log.info("消费者不再需要更多数据,给生产者发送中断信号");
producer.canceled = true;
}
}
@Slf4j
class Producer implements Runnable{
public volatile boolean canceled = false;
BlockingQueue dataQueue;
public Producer(BlockingQueue dataQueue) {
this.dataQueue = dataQueue;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
dataQueue.put(num);
log.info("num : {} 是100的倍数", num);
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
log.info("【生产者】运行结束");
}
}
}
@Slf4j
class Consumer{
BlockingQueue dataQueue;
public Consumer(BlockingQueue dataQueue) {
this.dataQueue = dataQueue;
}
public boolean canConsumer(BlockingQueue dataQueue){
if(Math.random()>0.95){
return false;
}
return true;
}
}
上述代码有点复杂,是模拟了一个简单的生产者和消费者的简单实例,但是消费者消费数据比较慢,由于消费者消费数据不及时,导致生产者最终会进入到阻塞状态,在队列数据满了之后,生产者阻塞在dataQueue.put() 方法中了,这个时候,生产者是无法判断代码中volatile修饰的标志位变量的,因此依旧阻塞,无法停止。运行结果如下:
从运行结果来看,线程并没有终止。因此这种情况volatile并不适用,如果要正确停止上述线程,用interrupt的方法即可。
interrupt相关方法
有很多方法与interrupt方法比较相似,具体如下
1、static boolean interrupted()
2、boolean isInterrupted()
3、Thread.interrupted()的目的对象
第一个静态方法(注意,这个方法不是interrupt),判断线程是否中断,返回判断结果之后,会将线程的中断状态置为false
第二个方法,判断线程是否中断,返回判断结果之后,不会将线程的中断状态置为false
第三个方法,需要注意的是,这个其实就是第一个方法,只是调用的时候不是通过具体的对象来调用,而是通过Thread类进行调用。
public class RightWayInterruptMethod {
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.");
}
}
运行结果
第二行和第三行输出结果,不论调用interrupted 方法的是对象还是Thread类,其判断结果都是当前线程的中断标志位的结果。而当前的运行线程是main线程,故而是false。
常见的面试问题
1、如何停止线程
用interrupt来操作,而不是所谓的stop和volatile等方式,interrupt本身在一定程度上可以保证数据安全,要想达到正确停止线程的效果,除了简单发出中断信号还不行,还需要目标线程进行配合,通知目标线程对中断异常也要做一个正取处理,不能在子方法中将静默处理,如果在子方法中处理也要重新开中断。volatile在一定场景下可以正常停止,但是在目标线程阻塞的时候,volatile变量停止线程的操作,就不那么优秀了。
2、如何处理不可中断的阻塞
interrupt方法并不是万能的,在目标线程进行socket io操作的时候,线程长期处于阻塞状态,但是这种阻塞并没有一个通用的解决方案,这种时候,我们就需要设置IO超时的时间,让线程能做到可以响应中断。
可以响应中断的方法
除了sleep方法,还有些方法,目标线程在执行该指定方法的时候,虽然不是运行状态,但是也可以响应中断。这些方法如下:
1、Object.wait()/wait(long)/wait(long,int)
2、Thread.sleep(long)/sleep(long,int)
3、Thread.join()/join(long)/join(long,int)
4、java.util.concurrent.BlockingQueue.take()/put(E)
5、java.util.concurrent.locks.Lock.lockInterruptibly()
6、java.util.concurrent.CountDownLatch.await()
7、java.util.concurrent.CyclicBarrier.await()
8、java.util.concurrent.Exchanger.exchange(V)
9、java.nio.channels.InterruptibleChannel相关方法
10、java.nio.channels.Selector的相关方法
总结
关于停止,掌握上面梳理的内容,应该差不多了,后续总结线程的几种状态
|