Java之Interrupt解析

1,483 阅读7分钟

前言

大家在实际工作中,在多线程的场景下,肯定或多或少的遇到和处理过InterruptedException,但是很多人不清楚该异常底下实际的实现原理。本文主要的内容就是结合openJdk源码,来详解java世界的中断机制

Java中的线程中断

事实上,在Java中,线程中断是唤醒某些JDK方法调用的一种方式,当你调用Thread.interrupt()时,他真正的操作是:

  • 将该线程thread对象中的interrupted 标记位置为true。

这只是其中一个操作,当触发执行interrupt()函数时,还会有其他事情发生:

  • 如果线程正好正在执行Object.wait(), Thread.join(), or Thread.sleep()和其他重载了这些这方法时,线程会被立马中断。然后这些方法会清理interrupted标记位,然后抛出InterruptedException
  • 如果线程正在执行LockSupport.park(...)方法获取任意和他同类型的方法时(如parkUntil),该调用会更早的返回
  • 如果线程时阻塞在网络I/O上(如Selector.select()).NIO的chanel(或者socket)将会被关闭。需要注意的是,这仅仅适用JDK实现方式,而不是其他框架。

其他中断的行为都是基于这些机制,Future类,Executor,Lock和Condition的所有有关中断的行为都基于上面的机制。我们不禁要好奇,这一切是如何工作的

Interrupts 是如何工作的

Thread.interrupt()`到底做了什么,我们应该从源头找起 ,那就是Thead.java

  public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

// ...

private native void interrupt0();

整个代码很简单,我们晚点来讲blockerLock这个局部变量。从整体看,核心代码是在interrupt0()触发的。我们需要探究到native方法。让我们来看看Tread.c

// ...
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
// ...

然后跟踪进jvm.cpp中,

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  ThreadsListHandle tlh(thread);
  JavaThread* receiver = NULL;
  bool is_alive = tlh.cv_internal_thread_to_JavaThread(jthread, &receiver, NULL);
  if (is_alive) {
    Thread::interrupt(receiver);
  }
JVM_END

从上文中可以看到最终调用的是thread.cpp

 void Thread::interrupt(Thread* thread) {
  os::interrupt(thread);
}

最终在os_posix.cpp中结束

void os::interrupt(Thread* thread) {
  OSThread* osthread = thread->osthread();
  if (!osthread->interrupted()) {
    osthread->set_interrupted(true);
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }
  ((JavaThread*)thread)->parker()->unpark();
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;
}

我们最终追踪到了我们感兴趣的点, 从嗲吗中可以看出,会肩擦thread的interrupted标识位是否被设置,如果没有,则设为true。同时,会将线程通过unapcking来唤醒。同时可以看出,当并发执行中断时,他是线程安全的。另外,如果我们查看了set_interrupted(true)的源码,我们会发现只有这里会调用这个方法。可以反推出中断调用来自Tread.interrupt();

中断操作是如何唤醒Tread.sleep()

如果我们从Tread.sleep方法一路看调用链下来,我们最终都会在os_posix.cpp相遇

int os::sleep(Thread* thread, jlong millis, bool interruptible) {
  ParkEvent * const slp = thread->_SleepEvent ;

  if (interruptible) {
    jlong prevtime = javaTimeNanos();

    for (;;) {
      if (os::is_interrupted(thread, true)) {
        return OS_INTRPT;
      }
      jlong newtime = javaTimeNanos();
      if (newtime - prevtime >= 0) {
        millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;
      }
      if (millis <= 0) {
        return OS_OK;
      }
      prevtime = newtime;
      JavaThread *jt = (JavaThread *) thread;
      ThreadBlockInVM tbivm(jt);
      OSThreadWaitState osts(jt->osthread(), false /* not Object.wait() */);

      slp->park(millis);

      jt->check_and_wait_while_suspended();
    }
  }
}

突出的一点是,Java使用parking机制来实现thread的sleep。然后通过interrupt来unpark线程。JDK和JVM紧密继承,来实现整个机制.

中断如何唤醒Object.wait()

可以通过追述openJdk来查找,整个过程肯定和上面类似。众所周知,还有一种机制可以唤醒Object.wait(),那就是Object.notify(),通常来说,我们需要获取到那个实例的monitor才能调用从notify()。

Object.c

 //...    
 {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
 //...

然后进入jvm.cpp

JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
  JVMWrapper("JVM_MonitorWait");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  JavaThreadInObjectWaitState jtiows(thread, ms != 0);
  if (JvmtiExport::should_post_monitor_wait()) {
    JvmtiExport::post_monitor_wait((JavaThread *)THREAD, (oop)obj(), ms);
  }
  ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END

然后查看ObjectSynchronizer::wait

void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  if (UseBiasedLocking) {
    BiasedLocking::revoke_and_rebias(obj, false, THREAD);
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
  }
  if (millis < 0) {
    TEVENT (wait - throw IAX) ;
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  monitor->wait(millis, true, THREAD);

  /* This dummy call is in place to get around dtrace bug 6254741.  Once
     that's fixed we can uncomment the following line and remove the call */
  // DTRACE_MONITOR_PROBE(waited, monitor, obj(), THREAD);
  dtrace_waited_probe(monitor, obj, THREAD);
}

最后是ObjectMonitor.wait()

 // check if the notification happened
   if (!WasNotified) {
     // no, it could be timeout or Thread.interrupt() or both
     // check for interrupt event, otherwise it is timeout
     if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
       TEVENT (Wait - throw IEX from epilog) ;
       THROW(vmSymbols::java_lang_InterruptedException());
     }
   }

从这里看一看出当我们调用wait时,InterruptedException是从哪里抛出的。等待一个object实例,将线程状态变为sleep,直到某些有趣的事情发生了。通常有趣的事情是由用户自己定义的,然后通过notify()触发。

有时候,在一个waiting状态的线程调用interrupt()并不会起作用,而且thread似乎会忽略这个事件。这是由于在wait()结束返回的时候,thread必须要持有这个对象的。来看下面的例子:

Thread thread1 = new Thread() {
  public void run() {
  synchronized (lock) {
    while (!ready) {
      lock.wait();
    }
    // use data
  }
};
thread1.start();

wait()确保了我们持有lock对象,但是当我们的线程被中断时,waiter在抛出exception之前,必须要重新获得锁。这样才能确保在离开synchronized同步块时,lock是能被正常释放的。出问题的点是另外一个线程持有这个锁:

synchronized (lock) {
  thread1.interrupt();
  thread1.join();
}

这会导致死锁。这段代码想中断线程1,但是持有lock锁。Thread1 并不持有lock锁。一旦thread1被中断,他将会区尝试获取lock锁,然后抛出中断异常。但是thread2正在 持有lock锁,thread1只能等待,但是thread2在尝试join thread1(阻塞等待thread1执行完毕以后执行)。他们符合死锁的特征,都在持有对方想要的东西。

中断如何唤醒 Selector.select()

这可能是中断最有趣的场景,Selector.select()是一个阻塞调用等待一些网络操作完成(标识位变为true)。 他通过非阻塞I/O实现,在linux中,他是通过epoll_wait的系统调用实现,令人惊奇的是,有时候Java的中断会将它唤醒。

我们已经看过interrupt()只是设置了Java Thread对象的一个标识为,然后调用unpark()。 Unparking只是pthread_cond_signa的一个轻量级的封装。这意味着epoll_wait感知不到它,那到底是如何唤醒的呢?

猜想一: Event FDs

第一个想法是Event FDs。这些描述符被设计成能够唤醒内核中的polling操作。使用模式包含向poller(例如:poll,epoll,select) 注册FD.当一个线程向fd写入时,poller将会感知到并且唤醒。这个是Netty在实现自己的epoll时所使用的方式。 这种方式有两个问题: 通过查看OpenJdk的源码,epoller并没有做这个操作。唯一的FDs是被管道fds创建,这些被用于Selector.wakeup() ,但是和 Thread.interrupt()所做的事情并不一样。FDs不是合理的猜想

猜想二:Signals(信号量)

第二个猜想是Posix 信号量。这是由于在penJDK epoll 实现中有处理信号量的分支:点击。 然后,如果signals被用于结束网络操作的阻塞状态,那么在执行interrupt() 会导致signal()被调用,但是通过查看源码并没有发现

真相

Thread.interrupt()的具体实现中,我们有见到一个操作:

    Interruptible b = blocker;
    if (b != null) {
        interrupt0();           // Just to set the interrupt flag
        b.interrupt(this);
        return;
    }

当一个线程被中断时,有调用Interruptible类,在Selector的场景中,在阻塞之前就已经新建了一个Interruptible,如果Thread.interrupt() 被调用,那么channel将会被关闭。会造成阻塞中的线程唤醒自己。这也是文件读取时的工作机制,否则这个操作会不可中断。这也是LockSupport.park() 唤醒的方式

最后需要注意的一点:解除阻塞的代码在Java中发生得概率很高,为了使其正常工作,只能通过调用Thread.interrupt()引起中断。因此,在线程代码中放置一个断点应该足以发现我们的线程是如何被中断的。

结论

如上所述,在java中,线程中断需要JDK 和JVM组合在一起才能实现,在实际的代码中,我们尽量小心的处理他们。