Java多线程 07 - 线程的中断
发布于 / 2017-06-01
简介:Java线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、IO阻塞等);等待阻塞是调用wait()方法产生的,同步阻塞则是由同步块synchronized同步锁产生的,睡眠阻塞是由sleep()产生的。
1. interrupt()方法简介
Java线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、IO阻塞等);等待阻塞是调用wait()
方法产生的,同步阻塞则是由同步块synchronized同步锁产生的,睡眠阻塞是由sleep()
产生的。
要中断一个Java线程,可调用线程类(Thread)对象的实例方法interrupt()
;然而interrupt()
方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true
的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。
- 如果线程的当前状态处于阻塞状态,那么在将中断标志设置为
true
后,还会有如下三种情况之一的操作:
- 如果是
wait()
、sleep()
以及join()
三个方法引起的阻塞,那么它的中断状态会被清除并且会收到一个InterruptedException
异常。例如,线程通过wait()
进入阻塞状态,此时通过interrupt()
中断该线程;调用interrupt()
会立即将线程的中断标记设为true
,但是由于线程处于阻塞状态,所以该中断标记会立即被清除为false
,同时,会产生一个InterruptedException
的异常。 - 如果是
java.nio.channels.InterruptibleChannel
进行的IO操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException
; - 如果是轮询(
java.nio.channels.Selectors
)引起的线程阻塞,则立即返回,不会抛出异常。
- 如果线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为
true
。但在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true
,则此后一旦线程调用了wait()
、sleep()
以及jion()
三个方法中的一种,立马抛出一个InterruptedException
,且中断标志被清除,重新设置为false
。
通过上面的分析,我们可以总结,调用线程类的interrupt()
方法,其本质只是设置该线程的中断标志,将中断标志设置为true
,并根据线程状态决定是否抛出异常。因此,通过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();
- }
- private native void interrupt0();
- public boolean isInterrupted() {
- return isInterrupted(false);
- }
- public static boolean interrupted() {
- return currentThread().isInterrupted(true);
- }
- private native boolean isInterrupted(boolean ClearInterrupted);
interrupt()
内部调用了interrupt0()
方法,它的作用是Just to set the interrupt flag,即仅仅是设置中断标识位,interrupt0()
被native修饰的,由Java虚拟机实现的。isInterrupted()
方法唯一的作用只是测试线程是否已经中断,中断标识位的状态并不受到该方法的影响。isInterrupted()
最终调用的是isInterrupted(boolean ClearInterrupted)
,这个也是一个native方法,由Java虚拟机实现,参数ClearInterrupted
决定是否清除中断标识位,isInterrupted()
中传递的是false
,即不清除。interrupted()
与isInterrupted()
方法一样,唯一的区别在于interrupted()
的内部调用的是currentThread().isInterrupted(true)
,所以interrupted()
会清除线程的中断标识位。
2. interrupt()最佳实践
通常使用interrupt()
的最佳实践是,可以使用interrupt()
方法来修改isInterrupted()
返回的结果,根据isInterrupted()
返回的结果来决定某一部分任务的执行。如对于一个线程我们分为阻塞和非阻塞两种状态,针对该线程内某一部分任务,中断有以下的处理:
- 非阻塞的状态
在非阻塞状态下可以通过isInterrupted()
决定某一段任务是否执行:
- class InterruptThread extends Thread {
- @Override
- public void run() {
- // 循环执行,如果 isInterrupted() 为true则停止执行
- for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
- }
- }
如果该线程正在运行,在对该线程调用了interrupt()
操作(可能是线程自己调用,也可能是其他并行执行的线程调用),将会将其标志位改为true
,因此for循环将终止执行,进而执行剩下的任务。
- 阻塞状态
在上面的第一种情况中,虽然对非阻塞状态可控,但当该线程处于阻塞状态时,对其调用interrupt()
操作会抛出InterruptedException
异常,会使线程直接终止,因此我们需要对InterruptedException
异常进行捕获:
- class InterruptThread extends Thread {
- @Override
- public void run() {
- // 循环执行,如果 isInterrupted() 为true则停止执行
- for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
- /**
- * 设置中断状态
- * Thread.sleep()方法由于中断而抛出异常,它会清除中断标记,在下一次循环开始时,就无法捕获这个中断,
- * 因此需要在异常处理中,重新设置中断标记位,以便在循环条件中能够获取到这次的中断事件
- */
- System.out.println(Thread.currentThread().getName() + " isInterrupted (Begin): " + isInterrupted());
- Thread.currentThread().interrupt();
- System.out.println(Thread.currentThread().getName() + " isInterrupted (End): " + isInterrupted());
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
- }
- }
在上述代码中,对该线程中可能会被中断的循环操作进行了try…catch处理,如果该线程处于阻塞状态,对其调用interrupt()
操作抛出的InterruptedException
异常会被捕获,同时for循环将被终止执行,进而执行剩下的任务。
3. interrupt()案例
我们根据上述代码,分别编写阻塞和非阻塞两种情况下的测试案例。
3.1. 阻塞情况的interrupt操作
首先是阻塞情况下的interrupt操作,示例代码如下:
代码如下:
- class InterruptThread extends Thread {
- @Override
- public void run() {
- for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") isInterrupted (Begin): " + Thread.currentThread().isInterrupted());
- /**
- * 设置中断状态
- * Thread.sleep()方法由于中断而抛出异常,它会清除中断标记,在下一次循环开始时,就无法捕获这个中断,
- * 因此需要在异常处理中,重新设置中断标记位,以便在循环条件中能够获取到这次的中断事件
- */
- Thread.currentThread().interrupt();
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") isInterrupted (End): " + Thread.currentThread().isInterrupted());
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
- }
- }
- public class InterruptTest {
- public static void main(String[] args) {
- // 启动测试线程
- Thread thread = new InterruptThread();
- thread.start();
- // 主线程等待5秒
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 主线程开始执行对thread线程的中断
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") execute thread.interrupt().");
- thread.interrupt();
- // 查看thread线程状态
- System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted.");
- // 主线程等待1秒,以获取准确的thread线程状态
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(thread.getName() + " (" + thread.getState() + ") is interrupted now.");
- }
- }
某一次运行的结果如下:
- Thread-0 (RUNNABLE) executed 0
- Thread-0 (RUNNABLE) executed 1
- Thread-0 (RUNNABLE) executed 2
- Thread-0 (RUNNABLE) executed 3
- main (RUNNABLE) execute thread.interrupt().
- Thread-0 (TIMED_WAITING) is interrupted.
- Thread-0 (RUNNABLE) catch InterruptedException.
- Thread-0 (RUNNABLE) isInterrupted (Begin): false
- Thread-0 (RUNNABLE) isInterrupted (End): true
- Thread-0 (RUNNABLE) executed 4
- Thread-0 (RUNNABLE) continued.
- Thread-0 (TERMINATED) is interrupted now.
运行过程详解:
- 在main方法中,主线程开始执行,并且创建并启动了thread-0线程,然后主线程开始休眠1秒。
- 此时thread-0线程一直在执行并且打印
run()
方法循环体内的信息,由于循环体内存在休眠操作,因此thread-0线程会有大部分时间处于阻塞状态。 - 在主线程对thread-0线程执行了
interrupt()
操作,此时thread-0线程有很大几率正处于阻塞状态,因此会抛出一个InterruptedException
异常。因此会跳出循环体。 - thread-0线程继续执行剩余的代码。
3.2. 非阻塞情况的interrupt操作
在上述代码中,如果去掉第7行InterruptThread类中的Thread.sleep(300);
操作,在主线程对thread-0线程执行了interrupt()
操作时,thread-0线程可能正处于非阻塞状态,此时是不会抛出InterruptedException
异常的,因此不会终止for循环。如果要在interrupt()
操作之后终止for循环,还需要在for循环内设置!isInterrupted()
为false
时for循环终止的条件。因为非阻塞状态下,中断操作会直接将thread-0线程的中断标志位置为true
,此时就能保证for循环操作在非阻塞状态下都能够响应interrupt()
操作从而结束循环。测试代码如下:
- class InterruptThread extends Thread {
- @Override
- public void run() {
- for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") isInterrupted (Begin): " + Thread.currentThread().isInterrupted());
- /**
- * 设置中断状态
- * Thread.sleep()方法由于中断而抛出异常,它会清除中断标记,在下一次循环开始时,就无法捕获这个中断,
- * 因此需要在异常处理中,重新设置中断标记位,以便在循环条件中能够获取到这次的中断事件
- */
- Thread.currentThread().interrupt();
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") isInterrupted (End): " + Thread.currentThread().isInterrupted());
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
- }
- }
- public class InterruptTest {
- public static void main(String[] args) {
- // 启动测试线程
- Thread thread = new InterruptThread();
- thread.start();
- // 主线程等待5秒
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 主线程开始执行对thread线程的中断
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") execute thread.interrupt().");
- thread.interrupt();
- // 主线程等待1秒,以获取准确的thread线程状态
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 查看thread线程状态
- System.out.println(thread.getName() + " (" + thread.getState() + ") isInterrupted: " + thread.isInterrupted());
- }
- }
运行结果如下:
- Thread-0 (RUNNABLE) executed 0
- Thread-0 (RUNNABLE) executed 1
- Thread-0 (RUNNABLE) executed 2
- Thread-0 (RUNNABLE) executed 3
- main (RUNNABLE) execute thread.interrupt().
- Thread-0 (RUNNABLE) catch InterruptedException.
- Thread-0 (RUNNABLE) isInterrupted (Begin): false
- Thread-0 (RUNNABLE) isInterrupted (End): true
- Thread-0 (RUNNABLE) executed 4
- Thread-0 (RUNNABLE) continued.
- Thread-0 (TERMINATED) isInterrupted: false
可以发现,相对于前面阻塞的情况,Thread-0 (RUNNABLE) is interrupted.
显示出在对thread-0线程执行interrupt操作时,thread-0线程正处于RUNNABLE状态,但由于for循环对isInterrupted()
进行了判断,也会在thread-0线程调用interrupt操作时终止循环。
3.3. 综合处理的最佳实践
如果想要同时处理阻塞和非阻塞两种情况下的中断操作,我们就应该将上面两种情况综合起来,这也是使用interrupt的最佳实践,代码如下:
- class InterruptThread extends Thread {
- @Override
- public void run() {
- // 循环执行,如果 isInterrupted() 为true则停止执行
- for (int i = 0; !isInterrupted() && i < Integer.MAX_VALUE; i++) {
- try {
- Thread.sleep(300);
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") executed " + i);
- } catch (InterruptedException e) {
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") catch InterruptedException.");
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") isInterrupted (Begin): " + Thread.currentThread().isInterrupted());
- /**
- * 设置中断状态
- * Thread.sleep()方法由于中断而抛出异常,它会清除中断标记,在下一次循环开始时,就无法捕获这个中断,
- * 因此需要在异常处理中,重新设置中断标记位,以便在循环条件中能够获取到这次的中断事件
- */
- Thread.currentThread().interrupt();
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") isInterrupted (End): " + Thread.currentThread().isInterrupted());
- }
- }
- System.out.println(Thread.currentThread().getName() + " (" + this.getState() + ") continued.");
- }
- }
- public class InterruptTest {
- public static void main(String[] args) {
- // 启动测试线程
- Thread thread = new InterruptThread();
- thread.start();
- // 主线程等待1秒
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 主线程开始执行对thread线程的中断
- System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") execute thread.interrupt().");
- thread.interrupt();
- // 主线程等待1秒,以获取准确的thread线程状态
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 查看thread线程状态
- System.out.println(thread.getName() + " (" + thread.getState() + ") isInterrupted: " + thread.isInterrupted());
- }
- }
运行结果如下:
- Thread-0 (RUNNABLE) executed 0
- Thread-0 (RUNNABLE) executed 1
- Thread-0 (RUNNABLE) executed 2
- main (RUNNABLE) execute thread.interrupt().
- Thread-0 (RUNNABLE) catch InterruptedException.
- Thread-0 (RUNNABLE) isInterrupted (Begin): false
- Thread-0 (RUNNABLE) isInterrupted (End): true
- Thread-0 (RUNNABLE) continued.
- Thread-0 (TERMINATED) isInterrupted: false
上面的代码既对InterruptedException
异常进行了捕获,也使用isInterrupted()
进行了循环体是否继续的判断,可以保证在阻塞和非阻塞两种情况下都响应interrupt事件。
推荐阅读
Java多线程 46 - ScheduledThreadPoolExecutor详解(2)
ScheduledThreadPoolExecutor用于执行周期性或延时性的定时任务,它是在ThreadPoolExe...