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

2022-10-31 11:00:28 浏览数 (1)

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 线程对象和底层实现的沟通

代码语言:javascript复制
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 线程启动

代码语言:javascript复制
// Thread.start()对应JVM_StartThread
JVM_ENTRY(void, JVM_StartThread(...))
...
// 虚拟机创建JavaThread,该类内部会创建操作系统线程,然后关联Java线程
native_thread = new JavaThread(&thread_entry, sz);
...
// 设置线程状态为RUNNABLE
Thread::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 线程停止

代码语言:javascript复制
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_ThreadStop
Thread::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()

代码语言:javascript复制
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

代码语言:javascript复制
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 线程睡眠

代码语言:javascript复制
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 睡眠已经中断的线程

代码语言:javascript复制
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()

代码语言:javascript复制
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 线程中断

代码语言:javascript复制
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线程启动、停止、睡眠与中断

  1. 下篇文章给大家讲解的是探讨虚拟机运行时的java线程栈帧、Java/JVM沟通 ;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

0 人点赞