Java线程中断的本质深入理解

一、Java中断的现象
首先,看看Thread类里的几个方法:











public static boolean interrupted 测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
public boolean isInterrupted() 测试线程是否已经中断。线程的中断状态 不受该方法的影响。
public void interrupt() 中断线程。

上面列出了与中断有关的几个方法及其行为,可以看到interrupt是中断线程。如果不了解Java的中断机制,这样的一种解释极容易造成误解,认为调用了线程的interrupt方法就一定会中断线程。
其实,Java的中断是一种协作机制。也就是说调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个boolean的中断状态(不一定就是对象的属性,事实上,该状态也确实不是Thread的字段),interrupt方法仅仅只是将该状态置为true


代码如下:

public class TestInterrupt {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.interrupt();
System.out.println("已调用线程的interrupt方法");
}
static class MyThread extends Thread {
public void run() {
int num = longTimeRunningNonInterruptMethod(2, 0);
System.out.println("长时间任务运行结束,num=" + num);
System.out.println("线程的中断状态:" + Thread.interrupted());
}
private static int longTimeRunningNonInterruptMethod(int count, int initNum) {
for(int i=0; i<count; i++) {
for(int j=0; j<Integer.MAX_VALUE; j++) {
initNum ++;
}
}
return initNum;
}
}
}

一般情况下,会打印如下内容:
已调用线程的interrupt方法
长时间任务运行结束,num=-2
线程的中断状态:true
可见,interrupt方法并不一定能中断线程。但是,如果改成下面的程序,情况会怎样呢?


代码如下:

import java.util.concurrent.TimeUnit;
public class TestInterrupt {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.interrupt();
System.out.println("已调用线程的interrupt方法");
}
static class MyThread extends Thread {
public void run() {
int num = -1;
try {
num = longTimeRunningInterruptMethod(2, 0);
} catch (InterruptedException e) {
System.out.println("线程被中断");
throw new RuntimeException(e);
}
System.out.println("长时间任务运行结束,num=" + num);
System.out.println("线程的中断状态:" + Thread.interrupted());
}
private static int longTimeRunningInterruptMethod(int count, int initNum) throws InterruptedException{
for(int i=0; i<count; i++) {
TimeUnit.SECONDS.sleep(5);
}
return initNum;
}
}
}

经运行可以发现,程序抛出异常停止了,run方法里的后两条打印语句没有执行。那么,区别在哪里?
一般说来,如果一个方法声明抛出InterruptedException,表示该方法是可中断的(没有在方法中处理中断却也声明抛出InterruptedException的除外),也就是说可中断方法会对interrupt调用做出响应(例如sleep响应interrupt的操作包括清除中断状态,抛出InterruptedException),如果interrupt调用是在可中断方法之前调用,可中断方法一定会处理中断,像上面的例子,interrupt方法极可能在run未进入sleep的时候就调用了,但sleep检测到中断,就会处理该中断。如果在可中断方法正在执行中的时候调用interrupt,会怎么样呢?这就要看可中断方法处理中断的时机了,只要可中断方法能检测到中断状态为true,就应该处理中断。让我们为开头的那段代码加上中断处理。
那么自定义的可中断方法该如何处理中断呢?那就是在适合处理中断的地方检测线程中断状态并处理。


代码如下:

public class TestInterrupt {
public static void main(String[] args) throws Exception {
Thread t = new MyThread();
t.start();
// TimeUnit.SECONDS.sleep(1);//如果不能看到处理过程中被中断的情形,可以启用这句再看看效果
t.interrupt();
System.out.println("已调用线程的interrupt方法");
}
static class MyThread extends Thread {
public void run() {
int num;
try {
num = longTimeRunningNonInterruptMethod(2, 0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("长时间任务运行结束,num=" + num);
System.out.println("线程的中断状态:" + Thread.interrupted());
}
private static int longTimeRunningNonInterruptMethod(int count, int initNum) throws InterruptedException {
if(interrupted()) {
throw new InterruptedException("正式处理前线程已经被请求中断");
}
for(int i=0; i<count; i++) {
for(int j=0; j<Integer.MAX_VALUE; j++) {
initNum ++;
}
//假如这就是一个合适的地方
if(interrupted()) {
//回滚数据,清理操作等
throw new InterruptedException("线程正在处理过程中被中断");
}
}
return initNum;
}
}
}

如上面的代码,方法longTimeRunningMethod此时已是一个可中断的方法了。在进入方法的时候判断是否被请求中断,如果是,就不进行相应的处理了;处理过程中,可能也有合适的地方处理中断,例如上面最内层循环结束后。
这段代码中检测中断用了Thread的静态方法interrupted,它将中断状态置为false,并将之前的状态返回,而isInterrupted只是检测中断,并不改变中断状态。一般来说,处理过了中断请求,应该将其状态置为false。但具体还要看实际情形。

二、Java中断的本质
在历史上,Java试图提供过抢占式限制中断,但问题多多,例如已被废弃的Thread.stop、Thread.suspend和 Thread.resume等。另一方面,出于Java应用代码的健壮性的考虑,降低了编程门槛,减少不清楚底层机制的程序员无意破坏系统的概率。
如今,Java的线程调度不提供抢占式中断,而采用协作式的中断。其实,协作式的中断,原理很简单,就是轮询某个表示中断的标记,我们在任何普通代码的中都可以实现。 例如下面的代码:


代码如下:

volatile bool isInterrupted;
//…
while(!isInterrupted) {
compute();
}

但是,上述的代码问题也很明显。当compute执行时间比较长时,中断无法及时被响应。另一方面,利用轮询检查标志变量的方式,想要中断wait和sleep等线程阻塞操作也束手无策。
如果仍然利用上面的思路,要想让中断及时被响应,必须在虚拟机底层进行线程调度的对标记变量进行检查。是的,JVM中确实是这样做的。下面摘自java.lang.Thread的源代码:


代码如下:

public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
//…
private native boolean isInterrupted(boolean ClearInterrupted);

可以发现,isInterrupted被声明为native方法,取决于JVM底层的实现。
实际上,JVM内部确实为每个线程维护了一个中断标记。但应用程序不能直接访问这个中断变量,必须通过下面几个方法进行操作:


代码如下:

public class Thread {
//设置中断标记
public void interrupt() { ... }
//获取中断标记的值
public boolean isInterrupted() { ... }
//清除中断标记,并返回上一次中断标记的值
public static boolean interrupted() { ... }
...
}

通常情况下,调用线程的interrupt方法,并不能立即引发中断,只是设置了JVM内部的中断标记。因此,通过检查中断标记,应用程序可以做一些特殊操作,也可以完全忽略中断。

你可能想,如果JVM只提供了这种简陋的中断机制,那和应用程序自己定义中断变量并轮询的方法相比,基本也没有什么优势。

JVM内部中断变量的主要优势,就是对于某些情况,提供了模拟自动“中断陷入”的机制。
在执行涉及线程调度的阻塞调用时(例如wait、sleep和join),如果发生中断,被阻塞线程会“尽可能快的”抛出InterruptedException。因此,我们就可以用下面的代码框架来处理线程阻塞中断:


代码如下:

try {
//wait、sleep或join
}
catch(InterruptedException e) {
//某些中断处理工作
}

所谓“尽可能快”,我猜测JVM就是在线程调度调度的间隙检查中断变量,速度取决于JVM的实现和硬件的性能。

三、一些不会抛出 InterruptedException 的线程阻塞操作
然而,对于某些线程阻塞操作,JVM并不会自动抛出InterruptedException异常。例如,某些I/O操作和内部锁操作。对于这类操作,可以用其他方式模拟中断:
1)java.io中的异步socket I/O
读写socket的时候,InputStream和OutputStream的read和write方法会阻塞等待,但不会响应java中断。不过,调用Socket的close方法后,被阻塞线程会抛出SocketException异常。
2)利用Selector实现的异步I/O
如果线程被阻塞于Selector.select(在java.nio.channels中),调用wakeup方法会引起ClosedSelectorException异常。
3)锁获取
如果线程在等待获取一个内部锁,我们将无法中断它。但是,利用Lock类的lockInterruptibly方法,我们可以在等待锁的同时,提供中断能力。

四、两条编程原则
另外,在任务与线程分离的框架中,任务通常并不知道自身会被哪个线程调用,也就不知道调用线程处理中断的策略。所以,在任务设置了线程中断标记后,并不能确保任务会被取消。因此,有以下两条编程原则:
1)除非你知道线程的中断策略,否则不应该中断它。
这条原则告诉我们,不应该直接调用Executer之类框架中线程的interrupt方法,应该利用诸如Future.cancel的方法来取消任务。

2)任务代码不该猜测中断对执行线程的含义。
这条原则告诉我们,一般代码遇在到InterruptedException异常时,不应该将其捕获后“吞掉”,而应该继续向上层代码抛出。
总之,Java中的非抢占式中断机制,要求我们必须改变传统的抢占式中断思路,在理解其本质的基础上,采用相应的原则和模式来编程。

(0)

相关推荐

  • 15个高级Java多线程面试题及回答

    Java 线程面试问题 在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分.如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题.在投资银行业务中多线程和并发是一个非常受欢迎的话题,特别是电子交易发展方面相关的.他们会问面试者很多令人混淆的Java线程问题.面试官只是想确信面试者有足够的Java线程与并发方面的知识,因为候选人中有很多只浮于表面.用于直接面向市场交易的高容量和低延时的电子交易系统在本质上是并发的.下面这些是我在不同时间不同地点喜欢问的Jav

  • 图解Java线程的生命周期

    在Java中,线程有5中不同状态,分别是:新建(New).就绪(Runable).运行(Running).阻塞(Blocked)和死亡(Dead).它们之间的转换图如下: 上图有一个例外,调用yield()方法可以让当前处于运行状态的线程转入就绪状态.如果要测试某线程是否已经死亡,可以使用isAlive()方法,该方法在线程处于就绪.运行.阻塞时放回true,新建和死亡时返回false.不要试图对一个已经死亡的线程调用start()方法而重新启动,死亡就是死亡和人一样,不可能再生.还有也不要对一

  • JAVA生产者消费者(线程同步)代码学习示例

    一.问题描述 生产者消费者问题是一个典型的线程同步问题.生产者生产商品放到容器中,容器有一定的容量(只能顺序放,先放后拿),消费者消费商品,当容器满了后,生产者等待,当容器为空时,消费者等待.当生产者将商品放入容器后,通知消费者:当消费者拿走商品后,通知生产者. 二.解决方案 对容器资源加锁,当取得锁后,才能对互斥资源进行操作. 复制代码 代码如下: public class ProducerConsumerTest { public static void main(String []args

  • java通过共享变量结束run停止线程的方法示例

    stop()方法已经被弃用,原因是不太安全.API文档中给出了具体的详细解释.通过interrupted()方法打断线程.不推荐.通过共享变量结束run()方法,进而停止线程.如实例 复制代码 代码如下: public class ThreadInterrupt {    public static void main(String []args){        Runner run = new Runner();        run.start();        try {       

  • Java多线程的用法详解

    1.创建线程 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例.Thread构造函数: public Thread( );  public Thread(Runnable target);  public Thread(String name);  public Thread(Runnable target

  • Java多线程 实例解析

    Java多线程实例 3种实现方法 Java中的多线程有三种实现方式: 1.继承Thread类,重写run方法.Thread本质上也是一个实现了Runnable的实例,他代表一个线程的实例,并且启动线程的唯一方法就是通过Thread类的start方法. 2.实现Runnable接口,并实现该接口的run()方法.创建一个Thread对象,用实现的Runnable接口的对象作为参数实例化Thread对象,调用此对象的start方法. 3.实现Callable接口,重写call方法.Callable接

  • java 实现线程同步的方式有哪些

    什么是线程同步? 当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题. 实现同步机制有两个方法: 1.同步代码块: synchronized(同一个数据){} 同一个数据:就是N条线程同时访问一个数据. 2. 同步方法: public synchronized 数据返回类型 方法名(){} 就是使用 synchronized 来修饰某个方法,则该方法称为同步方法.对于同步方法而言,无需显示指定同步监视器,同步

  • java基本教程之java线程等待与java唤醒线程 java多线程教程

    本章,会对线程等待/唤醒方法进行介绍.涉及到的内容包括:1. wait(), notify(), notifyAll()等方法介绍2. wait()和notify()3. wait(long timeout)和notify()4. wait() 和 notifyAll()5. 为什么notify(), wait()等函数定义在Object中,而不是Thread中 wait(), notify(), notifyAll()等方法介绍在Object.java中,定义了wait(), notify()

  • java多线程之定时器Timer的使用详解

    定时的功能我们在手机上见得比较多,比如定时清理垃圾,闹钟,等等.定时功能在java中主要使用的就是Timer对象,他在内部使用的就是多线程的技术. Time类主要负责完成定时计划任务的功能,就是在指定的时间的开始执行某个任务. Timer类的作用是设置计划任务,而封装任务内容的类是TimerTask类.此类是一个抽象类,继承需要实现一个run方法. 通过查文档我们看到Timer有以下几个构造函数: Timer的方法以下这么多: 下面我们通过定时器来完成一个简单功能,就是在运行项目三秒后,在控制台

  • Java实现在不同线程中运行的代码实例

    本文实例讲述了Java实现在不同线程中运行的代码.分享给大家供大家参考,具体如下: start()方法开始为一个线程分配CPU时间,这导致对run()方法的调用. 代码1 package Threads; /** * Created by Frank */ public class ThreadsDemo1 extends Thread { private String msg; private int count; public ThreadsDemo1(final String msg, i

  • JAVA中 终止线程的方法介绍

    在Java的多线程编程中,java.lang.Thread类型包含了一些列的方法start(), stop(), stop(Throwable) and suspend(), destroy() and resume().通过这些方法,我们可以对线程进行方便的操作,但是这些方法中,只有start()方法得到了保留. 在Sun公司的一篇文章<Why are Thread.stop, Thread.suspend and Thread.resume Deprecated? >中详细讲解了舍弃这些方

  • Java通过在主循环中判断Boolean来停止线程的方法示例

    本文实例讲述了Java通过在主循环中判断Boolean来停止线程的方法.分享给大家供大家参考,具体如下: package Threads; /** * Created by Frank */ public class StopBoolean extends Thread { // 确保变化对其它线程可见(主要是主线程要可见) protected volatile boolean done = false; public void run() { while (!done) { System.ou

  • java多线程和并发包入门示例

    一.java多线程基本入门java多线程编程还是比较重要的,在实际业务开发中经常要遇到这个问题. java多线程,传统创建线程的方式有两种. 1.继承自Thread类,覆写run方法. 2.实现Runnable接口,实现run方法. 启动线程的方法都是调用start方法,真正执行调用的是run方法.参考代码如下: 复制代码 代码如下: package com.jack.thread; /** * 线程简单演示例子程序 *  * @author pinefantasy * @since 2013-

随机推荐