文章目录
- 从JDK源码探究Java线程与操作系统的交互
- 一、序言
- 二、线程基础概念
- 1、操作系统线程实现方式
- (1)内核级线程(Kernel-Level Thread)
- (2)用户级线程(User-Level Thread)
- (3)混合线程(Hybrid Thread)
- 2、并发与并行
- 3、线程生命周期
- (1)操作系统层面
- (2)Java层面
- 三、Java线程实现JDK源码剖析
- 1、Thread.start方法
- 2、本地start0方法
- 2.1 JNI技术
- 3、Thread.c源文件
- 4、jvm.cpp源文件
- 5、thread.cpp源文件
- 5.1 os::create_thread函数
- 6、业务流程
- 四、后记
从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
,定义了六种状态:
public enum State {
// 新创建的线程状态
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()
方法,源码如下:
public synchronized void start() {
// 线程初始状态为0,对应NEW(创建状态)
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 通知此线程即将启动,添加到线程组的线程列表
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()
方法源码如下:
private native void start0();
很明显,start0()
方法被native
关键字修饰,标明其是一个本地方法,通过JNI接口底层调用C或C++去了。
看到这里,可能直接劝退部分小伙伴,小豪在这里要死磕到底,直接下载JDK源码,干起来!
登录Oracle官网 -> 下载JDK源码(地址在这) -> 解压后IDEA打开,一气呵成。
打开后的JDK源码文件过多,我们应该怎么找呢?
2.1 JNI技术
首先既然Thread
类调用了本地方法,那它一定会先注册本地方法。其实在Thread
类创建的时候,其通过static
修饰的静态代码块,调用registerNatives()
方法完成本地方法的注册,对应源码如下:
public class Thread implements Runnable {
// 注册本地方法
private static native void registerNatives();
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 = new JavaThread(&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 = new JavaThread(&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()
方法:
static unsigned __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多线程与并发编程相关内容,创作不易,如果大家觉得内容对你有收获,不妨考虑关注关注小豪~