vlambda博客
学习文章列表

本文深入探讨虚拟机运行时的java线程启动、停止、睡眠与中断

Java线程
上节描述了虚拟机中各式各样的线程及其创建过程,其中尤为重要的是JavaThread,它是Java线程java.lang.Thread在JVM层的表示,包含很多重要数据。
JavaThread持有一个指向java.lang.Thread对象的指针,即oop(JavaThread::_threadObj),java.lang.Thread也持有一个指向JavaThread的指针(java.lang.Thread中的eetop字段),只是这个指针是以整数的形式表示,如代码清单4-5所示:
代码清单4-5线程对象和底层实现的沟通
JavaThread* java_lang_Thread::thread(oop java_thread) {// 通过线程对象获取JavaThread(返回long值,强制类型转换为JavaThread*)return (JavaThread*)java_thread->address_field(_eetop_offset);}class JavaThread: public Thread {oop _threadObj;// 通过JavaThread获取线程对象oop threadObj() const { return _threadObj; }...};
这样Java线程对象oop能很容易地得到JavaThread,反过来JavaThread也能很容易地得到线程对象。
JavaThread还持有指向OSThread的指针,OSThread即操作系统线程。线程可以看作执行指令序列的一个实体,指令的执行依赖指令指针寄存器和栈指针寄存器等,它们放到一起就称为线程上下文。如果线程上下文是由硬件提供,那么该线程称为硬件线程;如果线程上下文是由软件提供,那么该线程称为软件线程。硬件线程是指令执行的最终使能对象,一般一个处理器至少提供一个硬件线程,在现实中,一个处理器通常提供两个硬件线程。硬件线程数量对于现代操作系统是远远不够的,通常操作系统会在硬件线程之上构造出操作系统线程(内核线程),然后将操作系统线程映射到硬件线程上。不同的操作系统可能选择不同的映射方式,例如在Linux中,操作系统线程以M:N映射到硬件线程,而JavaThread以1:1映射到操作系统线程,此时JavaThread调度问题实际转化为操作系统调度内核线程的问题。
线程调度会不可避免地涉及线程状态的转换。在用户看来,Java线程有NEW(线程未启动)、RUNNABLE(线程运行中)、BLOCKED(线程阻塞在monitor上加锁)、WAITING(线程阻塞等待,直到等待条件被打破)、TIME_WAITING(同WAITING,等待条件新增超时一项)、TERMINATED(线程结束执行)6种状态。而虚拟机则对Java线程了解得更深刻,它不但知道线程正在执行,还知道线程正在执行哪部分代码:_thread_new表示正在初始化;_thread_in_Java表示线程在执行Java代码;_thread_in_vm线程在执行虚拟机代码;
_thread_blocked表示线程阻塞。
线程启动
Java层的Thread.start()可以启动新的Java线程,该方法在JVM层调用prims/jvm的JVM_StartThread函数启动线程,这个函数会先确保java.lang.Thread类已经被虚拟机可用,然后创建一个JavaThread对象。
创建完JavaThread对象后,虚拟机设置入口点为一个函数,该函数使用JavaCalls模块调用Thread.run(),再由Thread.run()继续调用Runnable.run(),完成这一切后,虚拟机设置线程状态为RUNNABLE然后启动,如代码清单4-6所示:
代码清单4-6线程启动
// Thread.start()对应JVM_StartThreadJVM_ENTRY(void, JVM_StartThread(...))...// 虚拟机创建JavaThread,该类内部会创建操作系统线程,然后关联Java线程native_thread = new JavaThread(&thread_entry, sz);...// 设置线程状态为RUNNABLEThread::start(native_thread);JVM_END// JVM_StartThread创建操作系统线程,执行thread_entry函数static void thread_entry(JavaThread* thread, TRAPS) {HandleMark hm(THREAD);Handle obj(THREAD, thread->threadObj());JavaValue result(T_VOID);// Thread.start()调用java.lang.Thread类的run方法JavaCalls::call_virtual(&result,obj, SystemDictionary::Thread_klass(), vmSymbols::run_method_name(), vmSymbols::void_method_signature(),THREAD);}// thread_native使用JavaCalls调用Java方法Thread.run()public class java.lang.Thread {private Runnable target;public void run() {if (target != null) {target.run(); // Thread.run()又调用Runnable.run()}}...}
简而言之,Thread.start()先用JNI进入JVM层,创建对应的JavaThread,再由JavaThread创建操作系统线程,然后用JavaCalls进入Java层,让新线程执行Runnable.run代码。对应的线程启动逻辑如图4-5所示。

图4-5 线程启动逻辑
线程停止
线程停止的机制比较特别。在Java层面,JDK会创建一个ThreadDeath对象,该类继承自Error,然后传给JVM_StopThread停止线程,如代码清单4-7所示:
代码清单4-7线程停止
JVM_ENTRY(void, JVM_StopThread(...))// 获取JDK传入的ThreadDeath对象,确保不为空oop java_throwable = JNIHandles::resolve(throwable);if(java_throwable == NULL) {THROW(vmSymbols::java_lang_NullPointerException());}...// 如果要待停止的线程还活着if (is_alive) {// 如果停止当前线程if (thread == receiver) {// 抛出ThreadDeath(Error)停止THROW_OOP(java_throwable);} else {// 否则停止其他线程,向虚拟机线程投递VM_ThreadStopThread::send_async_exception(java_thread, java_throwable);}} else {// 否则复活它(停止没有启动的线程是java.lang.Thread允许的行为)java_lang_Thread::set_stillborn(java_thread);}JVM_END
如果要停止的线程是当前线程,那么JVM_StopThread只是让它抛出ThreadDeathError,这意味着如果捕获Error那么线程是不会停止的,如代码清单4-8所示:
代码清单4-8反常的Thread.stop()
public class ThreadTest {public static void main(String[] args) {new Thread(()->{try{Thread.currentThread().stop();}catch (Error ignored){ }System.out.println("still alive");}).start();}}
如果停止的不是当前线程,则情况会复杂一些。JVM_ThreadStop向虚拟机线程投递一个VM_ThreadStop的操作,由虚拟机线程负责停止它,一如之前所说。如代码清单4-9所示,VM_ThreadStop是一个VM_Operation,它的执行模式是asnyc_safepoint,即发起操作的线程在向虚拟机线程队列投递VM_ThreadStop后可继续执行,仅当虚拟机线程执行VM_ThreadStop时才需要除了虚拟机线程外的所有线程都到达安全点。
代码清单4-9 VM_ThreadStop
class VM_ThreadStop: public VM_Operation {private:oop _thread; // 要停止的线程oop _throwable; // ThreadDeath对象public:...// 停止线程操作需要异步安全点Mode evaluation_mode() const { return _async_safepoint; }void doit() {// 位于全局停顿的安全点ThreadsListHandle tlh;JavaThread* target = java_lang_Thread::thread(target_thread());if(target != NULL && ...) {// 发送线程停止命令target->send_thread_stop(throwable());}}};
VM_ThreadStop::doit()中的“发送”二字可能有些迷惑性,毕竟位于安全点的除了虚拟机线程外的其他应用线程都停顿了,发送给停顿线程数据意义不大,因此它们无法被观测到。实际上,send_thread_stop()只是将JDK创建的ThreadDeath对象设置到目标线程JavaThread中的_pending_async_exception字段。紧接着目标线程执行每条字节码时会检查是否设置了_pending_async_exception字段,如果设置了则转化为_pending_exception,最后线程退出时会检查是否设置了该字段并根据情况调用Thread::dispatchUncaughtException()。
与Thread.resume()配套的Thread.suspend()的实现也使用了类似Thread.stop()的机制,前者可让一个线程恢复执行,后者可暂停线程的执行。Thread.suspend()会向VMThread的VMOperation队列投递一个执行模式为safepoint的VM_ThreadSuspend操作,然后等待VMThread执行该操作。
这种实现方式导致Thread.stop等接口具有潜在的不安全性。因为当ThreadDeath异常传播到上层栈帧时,上层栈帧中的monitor将会被解锁,如果受这些monitor保护的对象正处于不一致状态(如对象正在初始化中),其他线程也会看到对象的不一致状态。换句话说,这些对象结构已经损坏。使用损坏的对象造成任何错误结果并不奇怪,更糟糕的是这些错误可能在很久后才会出现,导致调试困难。基于这些原因,Thread.stop/resume/suspend接口被标记为废弃,不应该使用。结束线程的正确方式是让线程完成任务后自然消亡。
睡眠与中断
Thread.sleep()可以让一个线程进入睡眠状态,它在底层调用JVM_Sleep方法,如代码清单4-10所示:
代码清单4-10线程睡眠
JVM_ENTRY(void, JVM_Sleep(...))JVMWrapper("JVM_Sleep");// 如果睡眠时间<0,则抛出参数错误异常if (millis < 0) {THROW_MSG(...);}// 如果待睡眠的线程已经处于中断状态if (Thread::is_interrupted (...) && !HAS_PENDING_EXCEPTION) {THROW_MSG(...);}// 保存当前线程状态JavaThreadSleepState jtss(thread);// 如果睡眠时间为0,Thread.sleep()退化为Thread.yield()if (millis == 0) {os::naked_yield();} else {ThreadState old_state = thread->osthread()->get_state();thread->osthread()->set_state(SLEEPING);if (os::sleep(thread, millis, true) == OS_INTRPT) {if (!HAS_PENDING_EXCEPTION) {THROW_MSG(...);// 如果睡眠的时候有异步异常发生}}// 恢复之前保存的线程状态thread->osthread()->set_state(old_state);}JVM_END
Thread.sleep()首先确保线程睡眠时间大于等于零。接着还需要防止睡眠已经中断的线程,这种情况少见但也会发生,如代码清单4-11所示:
代码清单4-11睡眠已经中断的线程
public class ThreadTest {public static void main(String[] args) {Thread t = new Thread(()->{synchronized (ThreadTest.class){ }try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});synchronized (ThreadTest.class){t.start();t.interrupt();}}}
防止了异常情况后,如果Thread.sleep()检查睡眠时间为0则会退化为Thread.yield(),调用操作系统提供的线程让出函数[1],如果睡眠时间正常,会调用如代码清单4-12所示的os::sleep():
代码清单4-12 Posix的os::sleep()
int os::sleep(Thread* thread, jlong millis, bool interruptible) {ParkEvent * const slp = thread->_SleepEvent ;slp->reset() ;OrderAccess::fence() ;if (interruptible) {jlong prevtime = javaTimeNanos();for (;;) {// 检查是否中断if (os::is_interrupted(thread, true)) {return OS_INTRPT;}// 更精确的睡眠时间jlong newtime = javaTimeNanos();if (newtime - prevtime < 0) {} else {millis -= (newtime - prevtime)/NANOSECS_PER_MILLISEC;}if (millis <= 0) {return OS_OK;}prevtime = newtime;...// 进行睡眠slp->park(millis);}} else {... // 类似上面的可中断逻辑,只是少了中断检查}}
为了支持可中断的睡眠,HotSpot VM实际上是使用ParkEvent实现的[2]。同样地,HotSpot VM的线程中断也是使用ParkEvent实现的,如代码清单4-13所示:
代码清单4-13线程中断
void os::interrupt(Thread* thread) {OSThread* osthread = thread->osthread();// 如果线程没有处于中断状态,调用ParkEvent::unpark()通知睡眠线程中断if (!osthread->interrupted()) {osthread->set_interrupted(true);OrderAccess::fence();ParkEvent * const slp = thread->_SleepEvent ;if (slp != NULL) slp->unpark() ;}if (thread->is_Java_thread())((JavaThread*)thread)->parker()->unpark();ParkEvent * ev = thread->_ParkEvent ;if (ev != NULL) ev->unpark() ;}
ParkEvent是Java层的对象监控器(Object Monitor)语意的底层实现,也是虚拟机内部使用的同步设施的基础依赖。在虚拟机运行时随便打个断点,会看到大多数线程最后一层栈帧都是调用ParkEvent::park()随后阻塞。
ParkEvent还有个孪生兄弟Parker,用于在底层支持java.util.concurrent.*中的各种组件。关于这两者将会在第6章中详细讨论。现在可以简单认为ParkEvent::park()让线程阻塞等待,ParkEvent::unpark()唤醒线程执行。
代码清单4-12和代码清单4-13多次用到OrderAccess,该组件用于保证内存操作的连续性与一致性,它是Java内存模型(Java MemoryModel,JMM)的基础设施,有助于虚拟机消除编译器重排序和CPU重排序,实现JMM中的Happens-Before关系等。关于它的更多内容,也会在第6章详细讨论。
本文给大家讲解的内容是探讨虚拟机运行时的java线程启动、停止、睡眠与中断

本文转载自网易号【老博侃历史】,更多内容请点击“阅读原文”