目录
线程终止是一个稍微复杂的问题,我们分运行状态和阻塞状态两种情况讨论。
首先我们思考一下,线程在什么情况下会终止?一般来说有如下几种情况:
第一种:当run方法完成后线程终止
run方法中的内容执行完后线程一般就自动结束了。
第二种:使用stop方法强行终止
该方法会强制关闭正在执行的线程,这种方法是不推荐的,因为假如很多指令正在执行,很多重要操作可能尚未完成,如果强制停止会导致潜在问题,例如一些清理性的工作没完成,如文件,数据库等的关闭。
也就说调用 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()方法来中断线程。
第三种:通过发送信号来终止线程
其本质和开启类似,就是主线程给子线程发送一个可以关闭的信号,但是具体什么时候执行关闭由子线程决定。这就像你正在工作,女朋友突然打电话要你和她出去逛街,你说“稍等,我先将手上的工作完成”是一样的道理。也就是说main线程只给子线程发送信号来告知要结束,而不是暴力地直接将其停掉。具体是否要关闭由子线程根据自身状态决定是否停止。
那通过信号停止线程,具体工作是怎么样的呢?应用程序发送一个线程终止的信号给JVM,JVM处理之后转给操作系统,操作系统再转给CPU,CPU收到之后会自行决定是否终止,而不一定马上终止。CPU此时可能在执行某个原子操作,或者要完成finally的功能才终止操作等,也就是会等手头的工作完成再终止(也叫安全点 ,或者安全区域)。
在Java中,主要是通过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方法,不能直接看:
- private native void interrupt0();
- private native boolean isInterrupted(boolean ClearInterrupted);
结论
对于正在执行的线程,如何优雅地将其终止的: main线程无法确定子线程的当前状态,可以通过interrupt()指令来给其他线性发送终止信号,而接收方通过isInterrupted()来监听是否可以终止线程,收到信号之后可以自行决定是否终止。
那native方法具体是咋回事呢,我们后面讲解Unsafe时再看。
如果线程是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();
- }
- }
我们可以发现上面的代码是可以停止的,但是为什么所有睡眠的代码,都要求必须处理InterruptedException异常呢?就是为了利用异常机制来唤醒阻塞的线程的。
这说明虽然线程是阻塞模式,但是触发异常之后就能获得CPU时间片来响应中断,并终止。 假如代码块在一个while自旋中,如何停止呢? 看例子:
- public class InterruptDemo02 implements 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的异常,但是子线程并没有终止。
运行到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");
- }
执行结果是:
可以看到打印了,线程也停止了。
通过上面的代码我们可以看到,对于涉及线程阻塞的方法,例如Thread.join(),Thread.wait(),Thread.sleep()等等,都会抛出异常。之所以如此,是因为如果需要让一个处于阻塞的线程被中断,从而做出响应。
我们再强调一下,主线程想让子线程停止要通过interrupt给子线程发一个信号,告诉要中断了,而不是强制将其中断。触发复位是为了让子线程保持原来的状态,是否中断则有子线程在catch中决定。如果不处理就不中断,如果要中断就是再执行一次Thread.currentThread().interrupt();
上面这个逻辑可以这么想象:放假的早上你正在睡懒觉,你妈叫你起来吃饭(给子线程发一个中断睡眠的状态),你被临时叫醒了,告诉她你知道啦(子线程的中断被临时打破,并且给主线程一个响应,异常就是为了干这个的,而不是出错了),如果不给她回话,她可能会一直叫你,甚至砸你的门。但是决定权在你这里,如果你不搭理还会继续睡(睡眠复位),你还可以决定那我起床吧(再次给自己一个不睡眠信号),然后起床(真正执行中断睡眠,开始起床的操作)。 如果看图就这样子:
main线程通过interrupt()通过修改子线程的共享变量isInterrupted状态来告诉子线程要终止,但是自己并不知道子线程当前的状态,也不能让子线程强制终止。子线程根据这个变量来判断自己是否应该终止。
本节我们介绍了线程的几个基础问题,例如如何创建线程,生命周期是怎么样的。其中几个重点我们务必理解清楚:
1.开启线程为什么要用start(),用户线程、JVM、操作系统和CPU分别做了什么。
2.线程的生命周期有几种状态,每种状态的特征是什么,sleep、waiting和time_waiting有什么区别和联系。
3.如何优雅的终止线程?此时用户线程、JVM、操作系统和CPU又分别做了什么。
4.如何终止正在阻塞的线程?此时子线程和主线程是如何交互的。