导语
前面写了两篇比较基础的文章:关于创建线程的方式有几种和关于线程的启动正确姿势,感觉有点拿不出手,不过这些基础才是大厦的基石,所以不能忽略。既然前面说了线程的创建、启动,那么自然就要说说线程的终止,或者说是中断、停止。
貌似这个问题也是很多小伙伴,以前经常碰到的面试题,面试官会问怎么停止线程才是正确的。当然熟读兵法(源码)的小伙伴,肯定会说使用interrupt 方法,此时面试官可能会问,那为什么不用stop 方法停止线程呢?此时你便会对此方法的弊端进行一系列的阐述了。
当然上面的回答没什么问题,但是我们为什么使用interrupt 方法呢?怎么使用呢?使用时该注意什么呢?这些其实也是我们所要考虑的问题。所以本篇文章将会对关于如何能优雅的停止线程进行比较详细的说明,譬如对stop 方法和interrupt 方法进行解析对比,除了这两种方法还有什么方法可以停止线程,这样的停止操作有我吗问题等等。
一、关于停止线程错误的方式
前面我们说到了在面试中会遇到这样的问题,和一些简单的回答,但是用上面的那样的方式去回答,肯定不能博得面试官的眼球的。因此我们来进行一些比较详细的阐述,首先我们先说下停止线程错误的方式。说到错误的线程停止方式肯定就要提到stop、suspend、resume,这几个方法我们一般都很清楚用它来停止线程是错误的,那为什么呢?首先jdk中就已经把其标记为启用,就是建议你不要使用这些方法了,那么使用这些方法导致的后果是什么呢?譬如你在更新一组数据,当你使用这个三个方法的时候,就会破坏数据的完整性,导致数据 最后的结果不准确。这里我们要提一下其实resume 方法在stop 方法中是有调用的。那我们先用代码来演示一下使用stop 方法所产生的现象是什么(本节以stop方法为主来进行说明),代码如下:
public class StopThreadForStopMethod implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println( i + "班开始领取书籍");
for (int j = 1; j <=20 ; j++) {
System.out.print(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 StopThreadForStopMethod());
thread.start();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop();
}
}
上面代码所执行的后果会是:
1班开始领取书籍
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,1班领取结束
2班开始领取书籍
1,2,3,4,5,6,7,8,9,10,
从这样的结果中可以看出,当使用stop方法时,到导致2班在领取书籍时,还没领取完书籍,这个线程的操作就结束了,有人可能就会说,现实中还是可以继续的嘛!那我只能说你是个杠精。现实中或许是如此,那么假如你的run方法里面操作的是转账业务呢?你的银行账户刚转出去的钱,别人还没到账就把你的线程给干掉了,此时真是感觉酸爽,赔了夫人又择兵是不!那么为什么stop方法会出现这样的情况呢?官网的说法是:使用stop停止线程会导致其解锁已锁定的所有的监视器(当ThreadDeath异常传到栈里的时候,监视器将会被解锁),如果先前受监视器保护的对象便会出现不一致状态,而其他线程则会查看这些状态不一致的对象,如果对其进行操作,便会出现意外的结果。官方所表达的意思启示就是,调用stop方法会导致一些本该被锁保护的资源却被释放了,然后这些资源便可以被修改,这样所导致的结果就只这些数据最后状态的不一致性。我们来看看stop方法的源码:
@Deprecated
public final void stop() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
if (threadStatus != 0) {
resume(); // Wake up thread if it was suspended; no-op otherwise
}
stop0(new ThreadDeath());
}
public class ThreadDeath extends Error {
private static final long serialVersionUID = -4417128565033088268L;
}
这里最后调用的是stop0方法且同时传入了ThreadDeath这么个对象,而这个对象是继承自一个Error类的,这就说明这是个异常类,然后有人会问这个异常可以捕捉到嘛,然后对受损的数据进行修复呢?官网表示这个操做会提高多线程任务的代码量,总而言之就是不切实际的。
对于suspend方法调用,其最后结果和stop表现的一样,但它不会破坏对象,它会使线程挂起,此时它是带着锁休息的,因此它需要其他线程来对其进行唤醒,而如果准备来唤醒它的这个线程也需要这把锁的话,此时便会出现死锁现象(最后所调用的也是suspend0这个native方法),由于笔者电脑上暂且没有JVM源码,再次就不做赘述了。
其实有些资料上会讲到有用volatile设置boolean标志位来进行线程的停止,但是这个方法依然有他的缺陷,由于文章篇幅原因这里先不做过多阐述了,感兴趣的童鞋可以自己去学习一下,笔者后面可以的话,会单独写一篇短的博文进行讲述。
二、停止线程正确的姿势
1.普通情况下终止线程
前面我们简单的对停止线程的错误方式进行了说明,见后说了一下为什么这个方式是错误的,既然说这个方式是错的,那么自然要指出正确的方式是不,因此此时会对正确的方式来进行一个简单的演示,然后在说明其为什么会被推荐使用,以及使用过程中需要注意哪些问题。废话不多说,先来看段代码吧,如下:
public class StopThread implements Runnable {
@Override
public void run() {
int num = 0;
while (num <= Integer.MAX_VALUE / 2 && !Thread.currentThread().isInterrupted()) {
if (num % 100000 == 0) {
System.out.println(num + "是100000的倍数!");
}
num ++;
}
System.out.println("任务运行结束了!");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
上面的代码所做的工作是打印十万的倍数,首先先来对run方法中的代码逻辑进行一下解释,其实也很简单,但是笔者觉得还是需要说明一下,在while的判断里,首先判断此num计数小于等于Integer最大数值的半数,然后判断当前的Thread是否有被标记为interrupt的状态,如果以上两个条件都满足便跳出循环。如果判断力缺少!Thread.currentThread().isInterrupted() ,那么这个线程的任务是不会被中断的,这里我们就要说明一下interrupt方法的作用了,其实使用interrupt的时候,仅仅是通知需要被停止的线程“”你该被停止了“”,但被停止的线程自身拥有着何时停止、是否停止的决定权,这就是一种依赖请求方和被停止方都要遵守的一种编码规范。也就是一种写作机制。而我们上面所提到的stop方法便是一种粗暴的方法,会改变对象的一致性。因此这种协作式的方法是必要的,我们基本不希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。这便是使用interrupt的好处。
上面所说的是一种普通情况下需要停止线程,其实这也就是我们所说的线程在上面情况下停止的只用,那么如果出现阻塞时该怎么中断线程呢?
2.阻塞时该怎么中断线程
上面我们使用一个普通情况下停止线程的例子,来说明了一下interrupt方法的只用和它的作用与意义。但是我们除了这样普通的情况,就是我们的run方法中有sleep或wait方法时,来使用interrupt方法来中断线程,如下:
public class StopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 30 && !Thread.currentThread().isInterrupted()) {
if (num % 10 == 0) {
System.out.println(num + "是10的倍数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
3.每次迭代时都阻塞时终止线程的情况
这个意思就是:每次循环都会调用sleep或wait等方法,我们来看下代码:
public class StopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 3000) {
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(3000);
thread.interrupt();
}
}
这段代码和上面代码的不同点有:while判断条件建没有了!Thread.currentThread().isInterrupted() 、把sleep方法放到了while中,但是这样的情况下却依然能终止线程,此时可能就有童鞋说了,你之前不是说值调用interrupt方法不是不行嘛!为什么这里却可以了呢?这里当在每次的迭代中都有休眠的操作时,在迭代的判断代码中就不需要加入!Thread.currentThread().isInterrupted() 条件判断了,因为此时会在中断的过程中检测到中断的状态并且抛出异常。
其实关于interrupt的使用还有其他一些代码场景,这里我们就不多赘述了,因为我们本篇文章是用来阐述关于interrupt是正确的停止线程的方法,并对它的意义和作用进行了简单的说明。最后我们来看下interrupt的源码吧!
三、interrupt方法源码
其实本该在一开始的时候就来看源码的,但是还是觉得先说明错误和正确停止现在方法的代码演示和意义较好,但是现在看源码也不迟。
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
这段代码这的很简单,就是一些判断,最后还只调用了interrupt0()的native方法,前面我们也说明了interrupt方法并不是用来中断的方法,而是一种具有通知作用的协调方法。对于线程最后是否被停止,是被停止线程自己决定的。
总结
本文基本知识围绕stop方法和interrupt方法进行了一些比较,来说明他们的水才是更优雅的,顺便说了一些关于在哪些情况下终止线程,但其实关于interrupt还有很多的场景下会有不同的问题,但本文主题不是如此,后面如果有机会,笔者会再写一写关于interrupt方面的博文。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19405.html