前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >本文深入探讨虚拟机运行时的java线程启动、停止、睡眠与中断

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

作者头像
愿天堂没有BUG
发布2022-10-31 11:00:28
4820
发布2022-10-31 11:00:28
举报
文章被收录于专栏:愿天堂没有BUG(公众号同名)

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给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 愿天堂没有BUG 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java线程
  • 线程启动
  • 线程停止
  • 睡眠与中断
  • 本文给大家讲解的内容是探讨虚拟机运行时的java线程启动、停止、睡眠与中断
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档