线程的中断

1,684 阅读6分钟

『中断技术』其实是计算机系统中很重要的一个概念,甚至有人说,我们的操作系统就是「中断驱动的」。

中断,其实指的就是程序在执行过程中,发生了某些非正常的事件指示当前进程不能继续执行了,应当得到暂停或终止,而通知正在执行的进程暂停执行的这个操作就叫『中断』。

中断同时也是我们实现并发的基础,中断一个线程的执行,调度另一个线程的执行。

中断源

如果按照中断事件类型来分,大致上有以下几种类型的中断事件类型:

  • 机器故障中断事件。往往是电源故障、硬件设备连接故障等
  • 程序性中断事件。这种大多是我们的程序代码逻辑问题,导致的例如内存溢出、除数为零等问题
  • 外部中断事件。主要是时钟中断
  • 输入输出中断事件。设备出错或是传输结束

每一种类型的中断事件都对应一位二进制的比特位,系统中也对应一个中断寄存器用于保存当前系统所遇到的所有中断事件,1 表示该类型的中断事件发生,0 表示未发生。

中断操作主要分为两种方式,一种叫『抢占式中断』,一种叫『主动式中断』。前者就是在发生中断时,强制剥夺线程的 CPU,后者是在正在执行的线程中断位上标记一下,具体什么时候中断由线程自己来决定。

当线程发现自己有中断事件时,会根据中断事件的类型去对应相应的中断处理程序来处理该中断事件。

下面我们看几种类型的中断事件,对应的中断处理程序是如何处理的。

1、电源故障(掉电)

首先,当我们的系统丢失电源时,系统硬设备是能保证继续工作一小段时间的。这也是为什么你的用浏览器浏览这好几个标签,突然关机了,开机后打开浏览器会提示你上次异常关闭,问你是否恢复的原因。

而我们的中断处理程序首先会将当前所有寄存器中的数据经由主存保存到磁盘,接着停止 CPU 的运行,直至停机。

下次开机时,中断处理程序会从磁盘加载中断前的寄存器数据,恢复现场。

2、程序逻辑中断

当我们的 CPU 执行除运算时遇到除数为零,将产生一个中断事件,对应的处理程序会简单的将错误类型及信息进行一个返回。

内存溢出异常也是一样的处理。

中断线程

Java API 中线程相关的方法主要有三个:

public void interrupt()

public static boolean interrupted()

public boolean isInterrupted()

interrupt 方法表示中断当前线程,仅仅设置一下线程的中断标记位。interrupted 是一个静态的方法,它将返回当前线程的中断位是否被标记,如果是则返回 true 并清空中断标记位,否则返回 false。

isInterrupted 方法功能是类似于 interrupted 方法的,只不过无论当前线程是否被中断了,都不会清空中断标志位。

我们看一个例子:

public void test() {
    Thread thread = new Thread(){
        @Override
        public void run(){
            for (int i=0; i<50000; i++){
                System.out.println("i=" + i);
            }
        }
    };
    thread.start();
    thread.interrupt();
        
    thread.join();
}

这样一段代码,我们创建一个线程,该线程启动后打印 50000 个数字,但是我们的主线程中又会去中断该线程。

抢断式中断方式下,thread 线程可能只打印了几个数字,甚至还未开始执行打印操作就被剥夺了 CPU,提前结束生命周期。

而我们的 Java 中不推荐使用抢断式中断,倡导「一个线程的生命不应该由其他线程终止,应当由它自己选择是否停止」。所以,这段程序会成功打印 50000 个数字,即便 thread 线程的中断标记位已经被标记。

简单修改下,我们的代码即能响应中断:

image

每一次打印前都去检查一下自己的中断标记位是否为 true,判断自己是否被中断以采取相应的处理操作。

但是这仅仅是线程处于 RUNNABLE 状态下对于中断请求的响应情况,下面我们具体看看线程的其他状态下,面对中断请求的响应措施。

线程对于中断的响应

RUNNABLE

状态为 RUNNABLE 的线程是拥有 CPU 正在运行的线程,我们的 interrupt 方法仅仅会设置一下该线程的中断标志位,不会做任何其他的操作,关于你是否响应此中断,由你自己决定。

这一类型的代码,我们已经在上文介绍了,此处不再赘述了。

WAITING

WAITING 状态是线程在获得锁的前提下,正常运行过程中由于缺失一些条件而被迫释放锁,交出 CPU,阻塞到等待队列上,等待别人唤醒的一个状态。

这个状态下的线程一旦被别人 interrupt 中断,将直接抛出异常 java.lang.InterruptedException。我们看一段代码:

public void test1() {
    Object obj = new Object();
    Thread thread = new Thread(){
        @Override
        public void run(){
            synchronized (obj){
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };

    thread.start();
    //主线程等待 thread 线程获取 obj 对象锁并阻塞自己到等待队列
    Thread.sleep(2000);
    thread.interrupt();
}

程序直接抛出异常,并清空中断标志位

你可以思考一下,一个 WAITING 状态的线程被中断为什么要抛出一个异常?

其实还是那个理念,「任何线程都没有权利终止另一个线程的生命」,一个正在 WAITING 中的线程由于不具有 CPU 的使用权,你中断它,它永远都不会知道自己被中断了直到自己重新竞争到了锁并得到运行。

那么,我们的主线程在调用 interrupt 方法中断一个线程,当发现它的状态为 WAITING 时,将唤醒它并更改指令寄存器的值以指向异常代码块,期待你自己来处理这个中断。

这也是为什么 wait、sleep、join 这些方法必须处理一个受检查的异常 InterruptException 的原因,因为这些方法会阻塞线程,而如果在阻塞期间收到中断,你也应当提供中断的处理逻辑。

BLOCKED

BLOCKED 状态的线程往往是竞争某个锁失败,而阻塞在某个对象的阻塞队列上的线程。

这个状态的线程和 RUNNABLE 状态的线程一样,对于中断请求不做额外响应,仅仅设置一下中断标志位,具体什么时候处理中断需要程序自己去循环检测判断。

NEW/TERMINATE

对于这两个状态的线程进行中断请求,目标线程什么也不会做,就连中断标志位也不会被设置,因为 Java 认为,一个还未启动的线程和一个已经结束的线程,对于他们的中断是毫无意义的。

总结一下,以上就是我们『中断技术』的相关概念,它是一种线程间协作方式,理解它是为了更优雅的结束线程,使程序走在我们的预期之中。