文章目录
从JDK源码探究Java线程与操作系统的交互
一、序言
在多核处理器环境下,多线程与并发编程已经成为提升程序响应速度和吞吐量的关键手段,对于Java工程师而言,深入理解多线程与并发编程内部机制,是构建高性能、高可用系统的基石。
线程,想必大家已经太过熟悉,但我们Java中的线程底层具体是如何实现的呢?它与操作系统之间是否有关联呢?
本文小豪将带大家探究Java线程与操作系统的关系,从JDK源码剖析Java线程的创建机制,话不多说,我们直接进入正文,一探Java线程背后的奥秘。
二、线程基础概念
在剖析JDK源码之前,我们先回顾一下线程的基本概念。
线程(也称轻量级进程)是指操作系统中能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发执行多个线程,每条线程并行执行不同的任务。
1、操作系统线程实现方式
在操作系统中,线程的实现可以分为三种不同的模型,包括内核级线程、用户级线程和混合线程。
(1)内核级线程(Kernel-Level Thread)
内核级线程是由操作系统内核直接支持的线程。每个内核级线程都直接映射到一个独立的处理器核心上,由内核进行调度。内核线程具有以下特点:
- 线程管理的所有工作(创建和撤销)由操作系统内核完成
- 一个线程阻塞,不影响另一个线程的执行
- 操作系统内核提供一个应用程序设计接口API,供开发者使用内核线程
- 内核级线程之间的上下文切换比用户级线程之间的切换要慢,因为它们需要涉及内核态的操作
CPU执行线程的任务时,会为线程分配时间片,上下文切换指CPU从一个进程或者线程,到另一个进程或者线程的切换,上下文切换即内核线程之间的调度
(2)用户级线程(User-Level Thread)
用户级线程是由用户程序实现的线程,不直接由操作系统内核支持。用户级线程的创建、调度和管理都是在用户自己的程序线程库中完成的,操作系统是感知不到的。用户线程具有以下特点:
- 用户级线程的创建和上下文切换通常比内核线程快,因为它们不需要涉及内核态
- 用户线程库可以根据应用程序的需求进行定制,提供更灵活的线程管理策略,但所有的线程操作都需要由用户程序自己去处理,实现起来比较复杂
(3)混合线程(Hybrid Thread)
混合线程模型结合了内核级线程和用户级线程的特点。在这种模型中,应用程序创建的用户线程被映射到一组内核线程上。这样,每个用户线程都可以独立运行,同时还可以享受到内核级线程的稳定性和系统调用能力。混合线程模型具有以下特点:
- 混合线程模型结合了用户级线程的轻量级和灵活性以及内核级线程的稳定性和系统调用能力。
那在我们Java中,创建线程使用的具体是哪种模型呢,大家逐步往下看,后文将会抛开迷雾
2、并发与并行
聊到线程,自然也得聊到在Java中的多线程机制。
多线程机制,其本质上就是为了充分利用多核处理器的计算能力,提高CPU的利用率。
与多线程伴随的,还有并发和并行的概念:
- 并发:在同一时间段内,有多个任务在交替执行
- 并行:在同一时间段内,有多个任务同时执行
对于单核的CPU运行多线程来说,只能是多线程并发,多个线程轮流使用一个CPU资源(时间片切换很快,感觉是在同时处理线程任务,实际上某一个时间点只处理一个线程任务),不能够做到多线程并行。
而对于多核的CPU运行多线程来说,可以做到多线程并行,如现在是8核的CPU,则可以同时并行执行8个线程任务。
3、线程生命周期
这里再额外补充一下线程的生命周期,线程从创建到死亡,在操作系统层面和Java层面都有明确的生命周期模型,但它们之间有所不同。
(1)操作系统层面
在操作系统层面的线程生命周期共五种,分别是:
- 初始状态:线程已经被创建,但还没有被启动,只是被初始化出来了,还不允许分配CPU执行
- 可运行状态(就绪状态):线程被创建并启动,可以分配CPU去执行,线程正在等待操作系统CPU的调度
- 运行状态:线程获取到CPU的时间片,执行线程任务
- 阻塞状态:运行状态的线程被阻塞,放弃CPU的时间片,等待解除阻塞重新回到可运行状态争抢时间片
- 终止状态:线程执行完成或抛出异常后进入到终止状态,释放所占用的资源
(2)Java层面
而在Java中,
Thread
类中的枚举
State
,定义了六种状态:
publicenumState{// 新创建的线程状态NEW,// 可运行的线程状态RUNNABLE,// 被阻塞的线程状态BLOCKED,// 等待线程的线程状态WAITING,// 等待时间的线程状态TIMED_WAITING,// 已终止的线程状态TERMINATED;}
- NEW(创建状态):线程对象被创建,但还没有调用线程对象的
start()
方法 - RUNNABLE(可运行状态 + 运行状态):调用了线程对象的
start()
方法以后,线程会进入可运行状态 ,但还没有运行,当线程获得到CPU执行权,线程会进入运行状态。或者是其它线程运行后,从阻塞/等待/超时等待状态中回来,也会处于可运行状态 - BLOCKED(阻塞状态):被其它线程所阻塞,没获取到同步锁
- WAITING(等待状态):调用
wait()
、join()
等方法后的状态 - TIMED_WAITING(超时等待状态):调用
sleep(time)
、wait(time)
等方法后的状态 - TERMINATED(终止状态):线程的
run()
方法执行结束或调用stop()
方法或抛出异常后的状态
Java中,将操作系统层面的可运行状态与运行状态合并,统称RUNNABLE可运行状态。同时Java将操作系统中的阻塞状态详细划分为BLOCKED阻塞、WAITING等待和TIMED_WAITING超时等待三种状态,但对于我们来说,只要Java线程处于这三种状态中的一种,就认为其是已经没有CPU的使用权了。
三、Java线程实现JDK源码剖析
接下来进入我们的正题,Java线程创建的底层源码剖析。
首先在Java中实现线程,常用的有几种方式:
- 第一种是继承
Thread
类 - 第二种是实现
Runable
接口 - 第三种是实现
Callable
接口 - 第四种是使用线程池创建线程
当然这些知识太过基础,相信没有小伙伴还不懂如何创建线程的,这里就不做过多说明了。
另外我们也知道,这几种创建线程的方式,本质上最终也是通过
new Thread()
来创建线程对象,最后调用
start()
方法启动Java层面的线程。
于是,我们由
Thread
类的
start()
方法作为入口,逐步分析一下线程的实现原理:
1、Thread.start方法
进入
Thread
类的
start()
方法,源码如下:
publicsynchronizedvoidstart(){// 线程初始状态为0,对应NEW(创建状态)if(threadStatus !=0)thrownewIllegalThreadStateException();// 通知此线程即将启动,添加到线程组的线程列表
group.add(this);boolean started =false;try{// (核心)开启线程start0();
started =true;}finally{try{if(!started){
group.threadStartFailed(this);}}catch(Throwable ignore){/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */}}}
在源码中,首先判断了线程的状态,初始状态为
0
,对应NEW创建状态,如果该线程状态不为NEW创建状态,则直接抛出异常。
2、本地start0方法
之后将创建的线程加入到线程组中,紧接着调用了
start0()
方法,我们继续跟入,
start0()
方法源码如下:
privatenativevoidstart0();
很明显,
start0()
方法被
native
关键字修饰,标明其是一个本地方法,通过JNI接口底层调用C或C++去了。
看到这里,可能直接劝退部分小伙伴,小豪在这里要死磕到底,直接下载JDK源码,干起来!
登录Oracle官网 -> 下载JDK源码(地址在这) -> 解压后IDEA打开,一气呵成。
打开后的JDK源码文件过多,我们应该怎么找呢?
2.1 JNI技术
首先既然
Thread
类调用了本地方法,那它一定会先注册本地方法。其实在
Thread
类创建的时候,其通过
static
修饰的静态代码块,调用
registerNatives()
方法完成本地方法的注册,对应源码如下:
publicclassThreadimplementsRunnable{// 注册本地方法privatestaticnativevoidregisterNatives();static{registerNatives();}// xxx}
而Java调用C或C++的代码,采用的是JNI技术,JNI技术其中一个必要环节就是通过
javah
命令生成一个C++头文件(
JavaNativeInterface.h
),然后会在C或C++的源代码中导入生成的头文件,而生成的头文件的命名规则为包名_类名 。
Java中
Thread
类的包路径为
java.lang
,则生成头文件的文件名为
java_lang_Thread.h
3、Thread.c源文件
于是,我们在JDK源码中全局搜一下
java_lang_Thread.h
,果不其然,就在一个Thread.c文件下发现了它:
在这个文件中,我们看到JNI本地方法映射着许多
Thread
类中的方法,包括
start0
、
stop0
、
sleep
等:
static JNINativeMethod methods[]={{"start0","()V",(void*)&JVM_StartThread},{"stop0","(" OBJ ")V",(void*)&JVM_StopThread},{"isAlive","()Z",(void*)&JVM_IsThreadAlive},{"suspend0","()V",(void*)&JVM_SuspendThread},{"resume0","()V",(void*)&JVM_ResumeThread},{"setPriority0","(I)V",(void*)&JVM_SetThreadPriority},{"yield","()V",(void*)&JVM_Yield},{"sleep","(J)V",(void*)&JVM_Sleep},{"currentThread","()" THD,(void*)&JVM_CurrentThread},{"countStackFrames","()I",(void*)&JVM_CountStackFrames},{"interrupt0","()V",(void*)&JVM_Interrupt},{"isInterrupted","(Z)Z",(void*)&JVM_IsInterrupted},{"holdsLock","(" OBJ ")Z",(void*)&JVM_HoldsLock},{"getThreads","()[" THD,(void*)&JVM_GetAllThreads},{"dumpThreads","([" THD ")[[" STE,(void*)&JVM_DumpThreads},{"setNativeName","(" STR ")V",(void*)&JVM_SetNativeThreadName},};
其中
start0
对应着
JVM_StartThread
虚拟机函数,老样子,我们全局搜一下
JVM_StartThread
。
4、jvm.cpp源文件
在jvm.cpp文件下发现了它的身影:
源码中注释有点过长,先精简一下:
JVM_ENTRY(void,JVM_StartThread(JNIEnv* env, jobject jthread))JVMWrapper("JVM_StartThread");
JavaThread *native_thread =NULL;bool throw_illegal_thread_state =false;{
MutexLocker mu(Threads_lock);if(java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread))!=NULL){
throw_illegal_thread_state =true;}else{
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
size_t sz = size >0?(size_t) size :0;// 直接看这里
native_thread =newJavaThread(&thread_entry, sz);if(native_thread->osthread()!=NULL){
native_thread->prepare(jthread);}}}if(throw_illegal_thread_state){THROW(vmSymbols::java_lang_IllegalThreadStateException());}assert(native_thread !=NULL,"Starting null thread?");if(native_thread->osthread()==NULL){delete native_thread;if(JvmtiExport::should_post_resource_exhausted()){JvmtiExport::post_resource_exhausted(
JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,"unable to create new native thread");}THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),"unable to create new native thread");}Thread::start(native_thread);
JVM_END
我们发现在这段源码中,有一行代码很显眼,不出意外,应该就是在这里开启的Java线程:
native_thread =newJavaThread(&thread_entry, sz);
5、thread.cpp源文件
继续往下找
JavaThread
函数,之后在thread.cpp文件下找到了对应的
JavaThread
函数:
在
JavaThread
函数中,最终,我们发现是通过os创建的线程,os即对应操作系统。
os::create_thread(this, thr_type, stack_sz);
看到这,想必大家也都悟了,Java创建线程底层其实是调用操作系统的内核级线程,Java线程与操作系统线程一一对应。
Java创建线程 -> 调用C++ -> 操作系统内核级线程
5.1 os::create_thread函数
最后,我们再搜一下
os::create_thread
函数对应的实现:
没错,在不同的操作系统下,JDK都适配了对应的创建线程函数,以windows为例,我们在os_windows.cpp文件下,在其实现的
os::create_thread
函数中,又调用了
java_start
函数,最终调用了线程的
run()
方法,对应着我们Java创建线程时实现的
run()
方法:
staticunsigned __stdcall java_start(Thread* thread){// xxx
__try {// 此处实际上调用了Java中对应的run方法
thread->run();}__except(topLevelExceptionFilter((_EXCEPTION_POINTERS*)_exception_info())){// Nothing to do.}// xxx}
6、业务流程
最后,我们大致梳理一下流程:
- 在Java中通过
Thread
类创建线程,调用其start()
启动线程 Thread
类start()
方法实际上调用本地方法start0()
,然后调用C++中JVM_StartThread()
函数创建并启动线程- 而后C++继续调用
JavaThread()
函数,根据不同的操作系统,调用各自的os::create_thread
函数完成线程创建 - 最终执行
thread->run()
函数回调Java中自定义线程实现的run()
方法
四、后记
本文从线程的基础概念开始介绍,过程中扩展了操作系统及Java层面线程的生命周期,最后带大家从JDK源码探究了Java线程的底层实现。
Java创建的线程最终调用的其实是操作系统的内核级线程,这也解释了为何在Java会将操作系统层面线程的可运行状态和运行状态统一合并为RUNNABLE状态,因为对于Java层面来说,会将线程调度交给操作系统去处理,而Java创建的线程,何时获取到操作系统CPU分配的时间片,是由操作系统决定的,在Java层面是无法控制的,无法界定这两个状态。
下一篇,小豪将会继续更新Java多线程与并发编程相关内容,创作不易,如果大家觉得内容对你有收获,不妨考虑关注关注小豪~
版权归原作者 Code豪客 所有, 如有侵权,请联系我们删除。