CPU 在运行时为了响应外部的请求,对外提供了一个中断引脚。CPU 在每个指令周期的最后一个晶振周期检查中断引脚,如果有中断任务,则立即停止手中的工作(当然要先保存现场)调用相应中断号的中断处理程序对中断做出响应。
进程在运行时为了响应外部请求,对外提供了信号队列。在每次由核心态转为用户态(比如由进程调度方法转到用户进程)时,会先检查自己的信号队列是否存在外部发来的信号,如果有则调用对应信号的信号处理程序对信号做出响应(Linux 下由 OS 在调度某进程前检查其信号队列,如果存在需要处理的信号则开启一个新线程调用信号处理程序)。
程序在运行过程中总是需要对外部发来的信号做出响应。因为这些信号是脱离于既定的运行逻辑之外的,我们不知道什么时候会发生这些事件,所以我们只能在现有调度方式的基础上,周期性的检查是否有信号到达。对中断信号的检查是基于处理器对指令的调度方式,每条指令的逻辑执行完成时去检查是否中断信号发生;对进程信号的检查是基于 OS 对进程的调度方式,每次调度该进程前检查是否有信号发生。
JVM 在线程层面为我们提供了类似的方法,线程的 interrupt 信号。
但是对于 interrupt 信号的处理与上面两位有很大的不同,线程的调度是基于 OS 的,JVM 除调用 OS 的系统函数外很难在其调度机制上做文章。
比如我们无法干预 OS 在调度线程运行前应该先去做点什么。
另外无论是中断处理程序还是信号处理程序,都是既定任务外的事件,对线程来讲,这些异步任务的处理是在其它线程中进行的,仅仅是延缓了本线程的调度,并没有改变本线程的任务。或者从底层说,没有改变本线程的堆栈/PC等现场。
这样我们就无法干预本线程的运行,比如将其从死锁中解放出来/终止当前方法的调用回滚堆栈。
interrupt 却在一定程度上做到了这一点。JVM 无法干预 OS 对线程的调度,但 JVM 可以从自身调度线程的层面上,在自己提供的基于 OS 线程调度的那些方法上做文章。
比如 sleep/wait/join 均是基于 OS 阻塞线程的方法对线程进行了阻塞,那么以 sleep 为例,我们可以这样写,伪代码:
public void sleep(Long sleepTime){ if(sleepTime<=0){ throw new RunTimeException("xxx"); } //OS阻塞线程的方法 os_park(sleepTime); if(interrupt==true){ throw new InterruptedException("xxx"); } }
在上述标红的地方调用 OS 阻塞线程的方法,线程运行到这一句是会阻塞住。
那么我们发送 interrupt 信号时线程会继续在 sleep 方法内向下执行,去检查 interrupt 信号,如果信号存在,抛出异常。这样便在本线程内,做出了对信号的响应。
事实上 JVM 也是这样做的,hotspot源码srcsharevmprimsjvm.cpp:
接着看其内部的sleep:
基于 ParkEvent 的 unpark 和 park 阻塞和唤醒对象,但在阻塞对象之后和唤醒对象之前,JVM 提供的方法是可以做点什么的。
本质上,硬件/OS/JVM 对信号的响应是相通的,都是基于自身提供的调度程序运行的机制,在调度的间隙对信号进行检查和处理。JVM 对信号的检查和处理逻辑因为封装在调用线程调用的方法内部,所以可以在调用线程中处理这些信号,也就可以对调用线程做更多的事情,比如 interrupt() 方法发生在因 sleep/wait/join 方法而阻塞的线程上时,由 sleep/wait/join 方法在其调用线程中抛出异常。