多线程与高并发|4.如何终止线程

线程终止是一个稍微复杂的问题,我们分运行状态和阻塞状态两种情况讨论。
1 如何终止正在执行的线程 首先我们思考一下,线程在什么情况下会终止?一般来说有如下几种情况:
第一种:当run方法完成后线程终止
run方法中的内容执行完后线程一般就自动结束了。
【多线程与高并发|4.如何终止线程】第二种:使用stop方法强行终止
这是不推荐这个方法,因为假如很多指令在执行时,会有指令还没有执行,从而导致数据不完整。 为什么不建议用stop: 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。也就说调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。看个例子:

public class ThreadStopExample extends Thread { @Override public void run() { try { for (int i = 0; i < 1000000; i++) { System.out.println("running code " + i); } System.out.println("the code finished"); }catch (Throwable e){ e.printStackTrace(); } } ? public static void main(String[] args) throws InterruptedException { Thread thread = new ThreadStopExample(); thread.start(); Thread.sleep(10); thread.stop(); } }

该代码在执行的时候会抛出如下异常:
running code 327 running code 328 running code 329 java.lang.ThreadDeath at java.lang.Thread.stop(Thread.java:853) at part_b_inside.chapter2_thread_life.terminal_thread.ThreadStopExample.main(ThreadStopExample.java:20)

可以看到,该方法仅仅执行到i=329就结束了,后面的都还没有完成。如果这是一个线上的服务,例如正在处理账务等信息时就导致数据混乱,造成无法预知的问题,因此一定不能用stop()方法来中断线程。
第三种:通过发送信号来终止线程 其本质和开启类似,就是应用程序发送一个终止的信号给JVM,JVM做了处理之后转给OS,OS收到之后会自行决定是否终止,如何终止,而不一定马上终止。CPU此时可能在执行某个原子操作,或者要完成finally的功能才终止操作等,所以会等手头的工作完成再终止(也叫安全点 ,或者安全区域)。这其实就像你正在工作,女朋友突然打电话要你和她聊天,你说“稍等,我先将手上的工作完成”是一样的道理。也就是说main线程只给子线程发送信号来告知要结束,而不是暴力地直接将其停掉。具体是否要关闭由子线程根据自身状态决定是否停止。
那通过信号停止线程,具体工作是怎么样的呢?主要是通过interrupt和isInterruptted()。
在Thread中提供了一个interrupt()方法,从名字看表示中断,但实际上并不像stop()方法一样直接中断线程,而是向子线程发送一个中断的通知。例如,假如你是领导,对于在加班的同事,你会说”做完就下班了,其他的明天再说“。这就是你给他发的信号量,而不是强制让他走,同事可以根据自己的情况处理完再走,这个时间可能是一分钟,也可能是一小时,决定权在同事这里。这就是信号量的含义。这也是线程安全中断的基本模型。
与interrupt()想配合的就是isInterruptted(),功能是判断是否收到了可以中断的请求。例如有的人就等着领导走, 只要一走,立马开溜,这就是一直在执行isInterruptted()。
先看一个例子:
public class InterruptDemo implements Runnable { @Override public void run() { while (true) { //执行操作 } } }

很明显,上面的while是无法被结束,外界也无法将其结束。 所以这里要将true改成,能够判断当前线程的某个标记位,如果满足要求再继续循环,也就是这个代码:
public class InterruptDemo implements Runnable { @Override public void run() { //isInterrupted表示一个中断标记,默认是false while (!Thread.currentThread().isInterrupted()) { //执行操作 } } }

这样外部就可以通过设置该变量来终止了,像这个样子:
public class InterruptDemo implements Runnable { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { //执行操作 } } ? public static void main(String[] args) { Thread thread=new Thread(new InterruptDemo()); thread.start(); //这里将共享变量isInterrupted设置为true,从而终止子线程 thread.interrupt(); } }

具体是怎么实现的呢?很遗憾,两个都是native方法,不能直接看: 这里的isInterrupted是一个共享变量,也是native的:
private native boolean isInterrupted(boolean ClearInterrupted);

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的实现是:
private native void interrupt0();

对于正在执行的线程,线程终止的原理是: main线程无法确定子线程的当前状态,执行interrupt()指令就是改变isInterrupted的状态,Thread在执行的时候一直判断该字段来确定这个线程是否需要被终止。
那native方法又干了什么,到底怎么完成信号通知的呢?我们后面再看。
2.4.2 如何终止被阻塞的线程
如果线程是sleep,join和waiting等状态,此时没有获得CPU时间片,也就无法及时感知到isInterrupted状态的变化,此时该如何中断呢?例如,假如某个人正在睡觉,如果发了,下班走人的通知他根本不知道,例如这个代码:
public class InterruptDemo2 implements Runnable { @Override public void run() { try { TimeUnit.SECONDS.sleep(20000000); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new InterruptDemo2()); t1.start(); Thread.sleep(1000); t1.interrupt(); } }

注意上面的代码是在最新的JVM环境下是可以停止的,这是因为JVM做的优化,但是JVM设计者仍然做了防范,不知你是否注意到,所有睡眠的代码,都会要求必须处理InterruptedException异常,为什么要有这个异常呢?是否可以利用异常机制来中断阻塞的线程呢?
这说明虽然线程是阻塞模式,但是触发异常之后就能获得CPU时间片来响应中断,并终止。 假如代码块在一个while自旋中,如何停止呢? 看例子:
public class InterruptDemo02implements Runnable{ @Override public void run() { while(!Thread.currentThread().isInterrupted()){ //①默认false,不做处理时中断之后会线程还挂着,不会退出 try { TimeUnit.SECONDS.sleep(200); System.out.println("run ....."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("processor End"); } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new InterruptDemo02()); t1.start(); Thread.sleep(100); t1.interrupt(); } }

这个例子可以看到,抛出了sleep interrupt的异常,但是子线程并没有终止。
多线程与高并发|4.如何终止线程
文章图片

运行到100ms时,主线程触发了子线程的interrupt exception,这个异常会触发线程复位。
看下面的例子,特别是注释的内容:本来在子线程中Thread.currentThread().isInterrupted()是false的,当主线程执行t1.interrupt(); 时将其改成了true①。 子线程中的异常再次将Thread.currentThread().isInterrupted()改成了false②,所以子线程会继续循环,而不会停止。 如果要终止,③需要catch里再设置一次中断,也就是这样:
public void run() { while (!Thread.currentThread().isInterrupted()) { //①默认false,不做处理时中断之后会线程还挂着,不会退出 try { TimeUnit.SECONDS.sleep(200); System.out.println("run ....."); } catch (InterruptedException e) {//②,这里会将isInterrupted再改成true e.printStackTrace(); System.out.println("interrupt..."); Thread.currentThread().interrupt(); //③子线程自己设置一次中断,此时子线程才会真正中断 } } System.out.println("processor End"); }

执行结果是:
多线程与高并发|4.如何终止线程
文章图片

可以看到打印了,线程也停止了。
通过上面的代码我们可以看到,对于涉及线程阻塞的方法,例如Thread.join(),Thread.wait(),Thread.sleep()等等,都会抛出异常。之所以如此,是因为如果需要让一个处于阻塞的线程被中断,从而做出响应。
我们再强调一下,主线程想让子线程停止要通过interrupt给子线程发一个信号,告诉要中断了,而不是强制将其中断。触发复位是为了让子线程保持原来的状态,是否中断则有子线程在catch中决定。如果不处理就不中断,如果要中断就是再执行一次Thread.currentThread().interrupt();
上面这个逻辑可以这么想象:放假的早上你正在睡懒觉,你妈叫你起来吃饭(给子线程发一个中断睡眠的状态),你被临时叫醒了,告诉她你知道啦(子线程的中断被临时打破,并且给主线程一个响应,异常就是为了干这个的,而不是出错了),如果不给她回话,她可能会一直叫你,甚至砸你的门。但是决定权在你这里,如果你不搭理还会继续睡(睡眠复位),你还可以决定那我起床吧(再次给自己一个不睡眠信号),然后起床(真正执行中断睡眠,开始起床的操作)。 如果看图就这样子:
多线程与高并发|4.如何终止线程
文章图片

main线程通过interrupt()通过修改子线程的共享变量isInterrupted状态来告诉子线程要终止,但是自己并不知道子线程当前的状态,也不能让子线程强制终止。子线程根据这个变量来判断自己是否应该终止。

    推荐阅读