JVM 对 interrupt 信号的响应

  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源码\src\share\vm\prims\jvm.cpp:

   接着看其内部的sleep:

   基于 ParkEvent 的 unpark 和 park 阻塞和唤醒对象,但在阻塞对象之后和唤醒对象之前,JVM 提供的方法是可以做点什么的。

  本质上,硬件/OS/JVM 对信号的响应是相通的,都是基于自身提供的调度程序运行的机制,在调度的间隙对信号进行检查和处理。JVM 对信号的检查和处理逻辑因为封装在调用线程调用的方法内部,所以可以在调用线程中处理这些信号,也就可以对调用线程做更多的事情,比如 interrupt() 方法发生在因 sleep/wait/join 方法而阻塞的线程上时,由 sleep/wait/join 方法在其调用线程中抛出异常。

  多扯一点,wait 与 sleep 相似是使线程挂起。语义上,wait 是等待在某个对象挂起,可以通过该对象唤醒 wait 在它上的所有线程。怎么看,wait 都不包含竞争锁的语义,但 wait 前必须获得对象的锁。对于 wait 前必须获取到锁,我当前的理解是:1、是保证对对象上等待队列的操作的线程安全性;2、保证 wait、notify 的 happen-before

posted @ 2020-03-01 23:18  牛有肉  阅读(973)  评论(0编辑  收藏  举报