JAVA多线程10-基础篇-线程终止

本节摘要: 介绍线程中断的原理、阻塞方法以及代码示例

一、为什么需要中断

有哪些场景我们需要中断线程?通常有以下几点:

  1. 我们希望在指定的时间内完成任务,但是任务执行时间太久,例如调用第三方的接口,或者执行一个耗时的I/O操作,这时我们希望取消该任务
  2. 当多个线程执行一个任务,只要有一个执行成功即可,其他线程取消
  3. 尝试获取某个资源(例如获取对象锁),超过期望时间仍没有获取到,我们希望取消该任务

但是java并没有办法安全、直接的停止一个线程,不过java提供了中断机制,它为安全停止正在执行的任务提供了更大的灵活性。中断本质上是一种协作机制,之所以称为“协作”机制,是因为当对线程执行中断操作时,线程并不是立即中断,而是线程本身根据业务场景来自行判断,事实上,大多数情况下我们也不希望线程立即停止,例如:

  1. 一个线程正在修改一个状态,如果此时中断线程,会导致状态不一致
  2. 处理一批任务,要么全成功,要么全失败,如果执行一半时检测到中断状态,需要将之前的任务状态重置

因此,线程的中断可以理解为一种"告知",而非"命令"。举个例子:

电业局的人告知用户欠费了,有的用户响应很及时,马上就缴费;有的用户有些其它的事情要处理,要过几天才能缴费。当然,用户也可以不缴费,最终就会停电。大概就是这个道理,大家可以总行体会一下^_-。

二、阻塞方法

抛出InterruptedException的方法是阻塞方法,例如Thread.sleep(),Object.wait(),Thread.join()等。但是反过来阻塞方法不一定都抛出InterrruptedException异常,例如因进入锁块(synchronized)而阻塞的线程,并不抛出中断异常。

三、java中处理中断的方法

/**
*中断本线程,如果当前线程处于阻塞状态(如sleep,wait,join),调用线程的interrupt()方法,线程会清除中断状态,然后抛出中断异常
*
*/
public void interrupt()

/**测试线程的中断状态并返回,同时清理线程的中断状态,有点拗口^_^
*  换句话说,如果连续两次调用该方法,第二次返回false
*/
public static boolean interrupted()

/**测试线程是否被中断,如果被中断返回true,否则返回false*/
public boolean isInterrupted()

四、中断原理

每个线程都拥有一个boolean属性,用来表示该线程的中断状态,该属性的初始值为false,当另外一个线程调用Thread.interrupt()方法时,会出现以下两种情况:
1)如果线程执行的是阻塞方法(抛出中断异常的方法,如join(),wait(),sleep()),会清除中断状态,并抛出InterruptedException异常,迅速响应中断请求
2)如果是非阻塞方法,只是设置线程的中断状态,业务代码可以根据中断状态以及业务场景来处理,可以选择中断线程,也可以忽略不管,或者设置中断状态后继续执行。

五、代码示例

5.1 处理InterruptedException 示例1

public class InterruptedExample1 {

    public static void main(String[] args) {
        Thread t1 = new MyThread("t1");
        t1.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.interrupt();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--"
                + t1.getName() + "--" + t1.getState() + "--" + t1.isInterrupted());
    }
}
class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        int i = 0;
        try {
            while (true) {
                System.out.println(++i);
                sleep(100);
            }
        } catch (InterruptedException e) {
            System.out.println("catch InterruptedException");
            System.out.println(getName() + "--" + getState() + "--" + isInterrupted());
            Thread.currentThread().interrupt();
            System.out.println(getName() + "--" + getState() + "--" + isInterrupted());
        }
    }
}

程序输出:
1
2
3
4
5
catch InterruptedException
t1--RUNNABLE--false
t1--RUNNABLE--true
main--t1--TERMINATED--false

结果说明:

  1. 主线程创建t1线程,并启动t1,t1线程的主要工作是循环输出++i,每次休眠100ms
  2. 主线程休眠500ms,调用intterupt()方法中断t1线程
  3. t1线程在收到中断请求后,会清除中断状态,并抛出中断异常,因此在catch块第一次调用isInterrupted()返回false,为了保留线程的中断状态,调用了intterupt()方法,再次调用isInterrupted()方法返回true
  4. 主线程再次休眠500ms,等待t1线程的终止操作结束
  5. 主线程输出t1的状态为terminated,即t1线程已经终止

5.2 处理InterruptedException 示例2

public class InterruptedExample2 {
    public static void main(String[] args) {
        try {
            Thread t1 = new Thread(new TaskRunnable(), "t1");
            t1.start();
            TimeUnit.MILLISECONDS.sleep(100);
            t1.interrupt();
            System.out.println("ending");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class TaskRunnable implements Runnable {
    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted()) {
            try {
                System.out.println(++i);
                Thread.sleep(300);
            } catch (InterruptedException e) {
                System.out.println("catch the InterruptedException");
                Thread.currentThread().interrupt();//重新设置中断标记
            }
        }
    }
}

结果输出:
1
ending
catch the InterruptedException

结果说明:

  1. 主线程创建t1线程并启动,t1线程主要工作是通过while()循环输出++i,每次休眠300ms,循环条件是isInterrupted()方法
  2. 主线程休眠100ms,调用t1线程的interrupt()方法
  3. t1线程收到中断请求后,会清除中断状态,并抛出中断异常,因此需要在catch块来重新设置中断标记,使while循环条件为false,线程终止,如果不想终止线程,可以不必重新设置中断标记

5.3 中断非阻塞线程示例

public class InterruptedExample3 {
    public static void main(String[] args) {
        Thread t1 = new MyThread1("t1");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.interrupt();
    }
}
class MyThread1 extends Thread {
    public MyThread1(String name) {
        super(name);
    }
    @Override
    public void run() {
        while (true) {
            if (isInterrupted()) {
                System.out.println("isInterrupted is true");
            } else {
                System.out.println(" isInterrupted is false");
            }
        }
    }
}
结果输出:
isInterrupted is false
isInterrupted is false
isInterrupted is false
isInterrupted is false
isInterrupted is false
isInterrupted is false
.....
isInterrupted is true
isInterrupted is true
isInterrupted is true
isInterrupted is true
isInterrupted is true
....

结果说明:interrupt()方法只是设置中断标记,不会终止线程,程序会一直输出

5.4 不可中断的阻塞示例(因获取锁而阻塞)

public class InterruptedExample4 {
    private static final Object o = new Object();
    public static void test() {
        int i = 0;
        synchronized (o) {
            while (i < 5) {
                System.out.println(i++);
                Thread.yield();
            }
        }
    }
    public static void main(String[] args) {
        test();
        Thread t1 = new Thread(new MyRunnable2(o), "t1");
        t1.start();
        t1.interrupt();
    }
}
class MyRunnable2 implements Runnable {
    private Object o;
    public MyRunnable2(Object o) {
        this.o = o;
    }
    @Override
    public void run() {
        synchronized (o) {
            System.out.println("get the lock do something");
        }
    }
}

程序输出:
0
1
2
3
4
get the lock do something

结果说明:

  1. 主线程首先调用test(),这是一个加锁方法,获取o的对象锁后开始循环输出++i
  2. 主线程创建t1线程,然后启动,再中断t1线程
  3. t1线程的run方法会获取o的对象锁,如果主线程此时没有释放锁,t1线程将一直阻塞
  4. 虽然对t1执行了中断操作,但是线程并没有中断,当获取o的对象锁后,线程继续执行

5.5 中断因I/O阻塞的线程

这个示例可以参考<<java编程思想>>第21章的示例,解决方案就是关闭导致任务阻塞的底层资源,例如socket 连接。

六、代码示例总结

通过上面的示例,我们可以总结如下的代码形式:

6.1 while循环在try...catch..块内,这种方式适用于线程处于“阻塞”和“非阻塞”两种状态

   @Override
   public void run() {
       try {
           //根据isInterrupted()方法判断是否被中断
           while (!Thread.currentThread().isInterrupted()) {
              //do work
           }
       } catch (InterruptedException e) {
           //如果是阻塞方法(如join,wait,sleep)被中断,会catch中断异常,如果需要通调用
           //方中断状态,调用interrupt()方法设置中断状态为true
       }
   }

6.2 while循环在try...catch..块外

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                //do work
            } catch (InterruptedException e) {
                //如果希望线程终止,此处必须重新设置中断状态,否则死循环
                Thread.currentThread().interrupt();
            }
        }
    }

6.3 如果调用阻塞方法(不在Runnable.run()内)又不知道如何处理InterruptedException,最简单的办法是直接抛出中断异常,由调用方处理

 public static void test() throws InterruptedException {
        Thread.currentThread().wait();
    }

七、全篇总结

  1. 线程中断是一种协作机制
  2. 对于阻塞方法(如sleep,wait,join)抛出的异常,不应该吞掉,可以向上继续抛出异常,或者设置中断状态来保留中断证据
  3. 因进入锁块(synchronized)引起阻塞的线程,中断操作不会抛出中断异常,只是设置了线程的中断状态为true
  4. 因I/O阻塞的线程,通用解决方案就是关闭导致任务阻塞的底层资源

推荐阅读更多精彩内容