线程的中断(interrupt)机制

简介: 线程的中断(interrupt)机制


什么时候需要关闭一个线程?

下面简单的举例情况:

  1. 比如我们会启动多个线程做同一件事,比如抢12306的火车票,我们可能开启多个线程从多个渠道买火车票,只要有一个渠道买到了,我们会通知取消其他渠道。这个时候需要关闭其他线程
  2. 很多线程的运行模式是死循环,比如在生产者/消费者模式中,消费者主体就是一个死循环,它不停的从队列中接受任务,执行任务,在停止程序时,我们需要一种”优雅”的方法以关闭该线程
  3. 在一些场景中,比如从第三方服务器查询一个结果,我们希望在限定的时间内得到结果,如果得不到,我们会希望取消该任务。
  4. 总之,很多情况下我们都有关闭一个线程的需求,那么如何正确的关闭一个线程就是我们要研究的事情.


废弃的API

Thread.STOP()之类的api会造成一些不可预知的bug,所以很早便Deprecated了,真要纠结为什么请看这边文章为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?


线程中断API

Thread类定义了如下关于中断的方法:



线程对中断的反应

  1. RUNNABLE:线程在运行或具备运行条件只是在等待操作系统调度
  2. WAITING/TIMED_WAITING:线程在等待某个条件或超时
  3. BLOCKED:线程在等待锁,试图进入同步块
  4. NEW/TERMINATED:线程还未启动或已结束


RUNNABLE状态

如果线程在运行中,interrupt()只是会设置线程的中断标志位,没有任何其它作用。线程应该在运行过程中合适的位置检查中断标志位,比如说,如果主体代码是一个循环,可以在循环开始处进行检查,如下所示:

public class InterruptRunnableDemo extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // ... 单次循环代码
        }
        System.out.println("done ");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new InterruptRunnableDemo();
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}


WAITING/TIMED_WAITING

线程执行如下方法会进入WAITING状态:

public final void join() throws InterruptedException
public final void wait() throws InterruptedException

执行如下方法会进入TIMED_WAITING状态:

public final native void wait(long timeout) throws InterruptedException;
public static native void sleep(long millis) throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException

在这些状态时,对线程对象调用interrupt()会使得该线程抛出InterruptedException,需要注意的是,抛出异常后,中断标志位会被清空(线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。),而不是被设置。比如说,执行如下代码:

Thread t = new Thread (){
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        //exception被捕获,但是为输出为false 因为标志位会被清空
            System.out.println(isInterrupted());
        }
    }        
};
t.start();
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();//置为true

InterruptedException是一个受检异常,线程必须进行处理。我们在异常处理中介绍过,处理异常的基本思路是,如果你知道怎么处理,就进行处理,如果不知道,就应该向上传递,通常情况下,你不应该做的是,捕获异常然后忽略。

捕获到InterruptedException,通常表示希望结束该线程,线程大概有两种处理方式:

  1. 向上传递该异常,这使得该方法也变成了一个可中断的方法,需要调用者进行处理
  2. 有些情况,不能向上传递异常,比如Thread的run方法,它的声明是固定的,不能抛出任何受检异常,这时,应该捕获异常,进行合适的清理操作,清理后,一般应该调用Thread的interrupt方法设置中断标志位,使得其他代码有办法知道它发生了中断

第一种方式的示例代码如下:

//抛出中断异常,由调用者捕获
public void interruptibleMethod() throws InterruptedException{
    // ... 包含wait, join 或 sleep 方法
    Thread.sleep(1000);
}

第二种方式的示例代码如下:

public class InterruptWaitingDemo extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 模拟任务代码
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // ... 清理操作
                System.out.println(isInterrupted());//false
                // 重设中断标志位为true
                Thread.currentThread().interrupt();
            }
        }
        System.out.println(isInterrupted());//true
    }
    public static void main(String[] args) {
        InterruptWaitingDemo thread = new InterruptWaitingDemo();
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        thread.interrupt();
    }
}


BLOCKED

如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。我们看段代码:

public class InterruptWaitingDemo extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 模拟任务代码
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // ... 清理操作
                // 重设中断标志位
                Thread.currentThread().interrupt();
            }
        }
        System.out.println(isInterrupted());
    }
    public static void main(String[] args) {
        InterruptWaitingDemo thread = new InterruptWaitingDemo();
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        thread.interrupt();
    }
}

BLOCKED 如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。我们看段代码:

public class InterruptSynchronizedDemo {
    private static Object lock = new Object();//monitor
    private static class A extends Thread {
        @Override
        public void run() {
          //等待lock锁
            synchronized (lock) {
                //等待标志位被置为true
                while (!Thread.currentThread().isInterrupted()) {
                }
            }
            System.out.println("exit");
        }
    }
    public static void test() throws InterruptedException {
        synchronized (lock) {//获取锁
            A a = new A();
            a.start();
            Thread.sleep(1000);
      //a在等待lock锁,interrupt 无法中断
            a.interrupt();
            //a线程加入当前线程,等待执行完毕
            a.join();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        test();
    }
}

test方法在持有锁lock的情况下启动线程a,而线程a也去尝试获得锁lock,所以会进入锁等待队列,随后test调用线程a的interrupt方法并等待线程线程a结束,线程a会结束吗?不会,interrupt方法只会设置线程的中断标志,而并不会使它从锁等待队列中出来。

我们稍微修改下代码,去掉test方法中的最后一行a.join,即变为:

public static void test() throws InterruptedException {
    synchronized (lock) {
        A a = new A();
        a.start();
        Thread.sleep(1000);
        a.interrupt();
    }
    //lock锁释放后 A线程重队列中出来
}

这时,程序就会退出。为什么呢?因为主线程不再等待线程a结束,释放锁lock后,线程a会获得锁,然后检测到发生了中断,所以会退出。

在使用synchronized关键字获取锁的过程中不响应中断请求,这是synchronized的局限性。如果这对程序是一个问题,应该使用显式锁,java中的Lock接口,它支持以响应中断的方式获取锁。对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。



NEW/TERMINATE

如果线程尚未启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。比如说,以下代码的输出都是false。

public class InterruptNotAliveDemo {
    private static class A extends Thread {
        @Override
        public void run() {
        }
    }
    public static void test() throws InterruptedException {
        A a = new A();
        a.interrupt();
        System.out.println(a.isInterrupted());
        a.start();
        Thread.sleep(100);
        a.interrupt();
        System.out.println(a.isInterrupted());
    }
    public static void main(String[] args) throws InterruptedException {
        test();
    }
}


IO操作

如果线程在等待IO操作,尤其是网络IO,则会有一些特殊的处理,我们没有介绍过网络,这里只是简单介绍下。

  1. 实现此InterruptibleChannel接口的通道是可中断的:如果某个线程在可中断通道上因调用某个阻塞的 I/O 操作(常见的操作一般有这些:serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write)而进入阻塞状态,而另一个线程又调用了该阻塞线程的 interrupt 方法,这将导致该通道被关闭,并且已阻塞线程接将会收到ClosedByInterruptException,并且设置已阻塞线程的中断状态。另外,如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的 I/O 操作,则该通道将关闭并且该线程立即接收到 ClosedByInterruptException;并仍然设置其中断状态。
  2. 如果线程阻塞于Selector调用,则线程的中断标志位会被设置,同时,阻塞的调用会立即返回。

我们重点介绍另一种情况,InputStream的read调用,该操作是不可中断的,如果流中没有数据,read会阻塞 (但线程状态依然是RUNNABLE),且不响应interrupt(),与synchronized类似,调用interrupt()只会设置线程的中断标志,而不会真正”中断”它,我们看段代码

public class InterruptReadDemo {
    private static class A extends Thread {
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                try {
                    System.out.println(System.in.read())//wait input
                } catch (IOException e) {
                    e.printStackTrace();
                }    
            }
            System.out.println("exit");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        A t = new A();
        t.start();
        Thread.sleep(100);
        t.interrupt();
    }
}

线程t启动后调用System.in.read()从标准输入读入一个字符,不要输入任何字符,我们会看到,调用interrupt()不会中断read(),线程会一直运行。

不过,有一个办法可以中断read()调用,那就是调用流的close方法,我们将代码改为:

public class InterruptReadDemo {
    private static class A extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println(System.in.read());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("exit");
        }
        public void cancel() {
            try {
                System.in.close();
            } catch (IOException e) {
            }
            interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        A t = new A();
        t.start();
        Thread.sleep(100);
        t.cancel();
    }
}

我们给线程定义了一个cancel方法,在该方法中,调用了流的close方法,同时调用了interrupt方法,这次,程序会输出:

-1
exit

也就是说,调用close方法后,read方法会返回,返回值为-1,表示流结束。



如何正确地取消/关闭线程

1. 以上,我们可以看出,interrupt方法不一定会真正”中断”线程,它只是一种协作机制,如果 不明白线程在做什么,不应该贸然的调用线程的interrupt方法,以为这样就能取消线程。

2. 对于以线程提供服务的程序模块而言,它应该封装取消/关闭操作,提供单独的取消/关闭方法给调用者,类似于InterruptReadDemo中演示的cancel方法,外部调用者应该调用这些方法而不是直接调用interrupt。

3. Java并发库的一些代码就提供了单独的取消/关闭方法,比如说,Future接口提供了如下方法以取消任务:boolean cancel(boolean mayInterruptIfRunning);

4. 再比如,ExecutorService提供了如下两个关闭方法:

void shutdown();
List<Runnable> shutdownNow();

5. Future和ExecutorService的API文档对这些方法都进行了详细说明,这是我们应该学习的方式。



目录
相关文章
|
25天前
|
存储 监控 安全
深入理解ThreadLocal:线程局部变量的机制与应用
在Java的多线程编程中,`ThreadLocal`变量提供了一种线程安全的解决方案,允许每个线程拥有自己的变量副本,从而避免了线程间的数据竞争。本文将深入探讨`ThreadLocal`的工作原理、使用方法以及在实际开发中的应用场景。
49 2
|
1月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
86 2
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
31 1
|
2月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
30 2
|
3月前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
33 0
|
4月前
探索操作系统中的线程同步机制
【8月更文挑战第31天】在多线程编程领域,理解并实现线程同步是至关重要的。本文通过浅显易懂的语言和生动的比喻,带你走进线程同步的世界,从互斥锁到信号量,再到条件变量,逐步揭示它们在协调线程行为中的作用。我们将一起动手实践,用代码示例加深对线程同步机制的理解和应用。
|
5月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。
102 0
|
5月前
|
监控 Java
Java面试题:Java内存、多线程与并发工具包的深度探索,Java内存管理策略及其优化技巧,Java多线程并发控制的工具类与机制,Java并发工具包在实际项目中的应用
Java面试题:Java内存、多线程与并发工具包的深度探索,Java内存管理策略及其优化技巧,Java多线程并发控制的工具类与机制,Java并发工具包在实际项目中的应用
46 0
|
5月前
|
存储 并行计算 安全
Java面试题:Java内存管理、多线程与并发框架的面试题解析与知识点梳理,深入Java内存模型与垃圾回收机制,Java多线程机制与线程安全,Java并发工具包与框架的应用
Java面试题:Java内存管理、多线程与并发框架的面试题解析与知识点梳理,深入Java内存模型与垃圾回收机制,Java多线程机制与线程安全,Java并发工具包与框架的应用
87 0
|
5月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
96 0