从JDK源码探究Java线程与操作系统的交互

news2024/11/15 1:26:14

文章目录

  • 从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中实现线程,常用的有几种方式:

  1. 第一种是继承Thread
  2. 第二种是实现Runable接口
  3. 第三种是实现Callable接口
  4. 第四种是使用线程池创建线程

当然这些知识太过基础,相信没有小伙伴还不懂如何创建线程的,这里就不做过多说明了。

另外我们也知道,这几种创建线程的方式,本质上最终也是通过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类中的方法,包括start0stop0sleep等:

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、业务流程

最后,我们大致梳理一下流程:

  1. 在Java中通过Thread类创建线程,调用其start()启动线程
  2. Threadstart()方法实际上调用本地方法start0(),然后调用C++中JVM_StartThread()函数创建并启动线程
  3. 而后C++继续调用JavaThread()函数,根据不同的操作系统,调用各自的os::create_thread函数完成线程创建
  4. 最终执行thread->run()函数回调Java中自定义线程实现的run()方法

四、后记

本文从线程的基础概念开始介绍,过程中扩展了操作系统及Java层面线程的生命周期,最后带大家从JDK源码探究了Java线程的底层实现。

Java创建的线程最终调用的其实是操作系统的内核级线程,这也解释了为何在Java会将操作系统层面线程的可运行状态和运行状态统一合并为RUNNABLE状态,因为对于Java层面来说,会将线程调度交给操作系统去处理,而Java创建的线程,何时获取到操作系统CPU分配的时间片,是由操作系统决定的,在Java层面是无法控制的,无法界定这两个状态

下一篇,小豪将会继续更新Java多线程与并发编程相关内容,创作不易,如果大家觉得内容对你有收获,不妨考虑关注关注小豪~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1926726.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【DevOps系列】DevOps简介及基础环境安装

作者:后端小肥肠 目录 1. 前言 2. DevOps(详细介绍) 3. Code阶段工具 3.1 Git安装 3.2 GitLab安装 4. Build阶段工具 5. Operate阶段工具 5.1 Docker安装 5.2 Docker-Compose安装 6. Integrate工具 6.1 Jenkins介绍 6.2 Jenkins安…

8-1 搭建solidity开发环境,自己定制一个truffle

8-1 搭建solidity开发环境,自己定制一个truffle(react区块链实战) 从零开始搭建一个项目 自己实现一套类似truffle的自动编译系统,加深理解 此处可以跳过无需自己实现编译合约的模块,使用已有的truffle模块即可 项目…

SSM框架学习笔记(仅供参考)

(当前笔记简陋,仅供参考) 第一节课: (1)讲述了Spring框架,常用jar包,以及框架中各个文件的作用 (2)演示了一个入门程序 (3)解释了…

TS 入门(二):Typescript类型与类型注解

目录 前言回顾1. 基本类型数字类型 (number)字符串类型 (string)布尔类型 (boolean)空值和未定义 (null 和 undefined)任意类型 (any)unknown 类型any 与 unkown 区别 2. 数组和元组类型数组类型元组类型 3. 枚举类型4. 类型注解示例指定变量类型函数参数和返回值类型注解类型推…

在浏览器控制台中输出js对象,为什么颜色不同,有深有浅

打开console,输入自定义的javascript对象的时候,打开看发现对象的属性是深紫色,后面有一些对象是浅紫色的,比如Array对象和一堆SVG,HTML,CSS开头的对象,常用的prototype和__proto__也是浅紫色的。 请问这里深紫和浅紫…

9. Python3 Numpy科学计算库

Numpy是Python科学计算库的基础,主要包括: 强大的N维数组对象和向量运算。一些复杂的功能。与C和FORTRAN代码的集成。实用的线性代数运算、傅里叶变换、随机数生成等。 9.1 Numpy基础 Numpy的主要对象是一个均匀的多维数组。Numpy提供了各种函数。可以…

pxe高效网络批量装机

文章目录 一, PXE远程安装服务(一)三种系统装机的方式(二)linux装机1. 加载 Boot Loader2. 加载启动安装菜单3. 加载内核和 initrd4. 加载根文件系统5. 运行 Anaconda 安装向导 (三)实现过程&am…

STM32使用CubeMX创建HAL库工程文件

文章目录 1. STM32CubeMX 2. 界面介绍 3. 使用教程 新建工程 选择芯片界面 ​编辑 配置页面 引脚配置页面 引脚配置界面的颜色指示 配置RCC时钟参数 配置SYS参数 配置时钟树 Project Manager项目管理配置 生成工程文件 KEIL代码编写 1. STM32CubeMX STM32CubeM…

得物六宫格验证码分析

声明(lianxi a15018601872) 本文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 前言(lianxi a…

【自学网络安全】二、防火墙NAT智能选路综合实验

任务要求: (衔接上一个实验所以从第七点开始,但与上一个实验关系不大) 7,办公区设备可以通过电信链路和移动链路上网(多对多的NAT,并且需要保留一个公网IP不能用来转换) 8,分公司设备可以通过总…

LeetCode 算法:电话号码的字母组合 c++

原题链接🔗:电话号码的字母组合 难度:中等⭐️⭐️ 题目 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下(与电话按键相同)。注意 …

线程-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

线程 线程概念 简介 线程定义:线程是操作系统进行调度的最小单位,包含在进程内,是进程中的实际执行单元 线程特性:一个线程代表进程中的一个单一顺序控制流,即执行路径 多线程应用:一个进程可以包含多个…

LabVIEW红外热波图像缺陷检

开发使用LabVIEW开发的红外热波图像缺陷检测系统。该系统结合红外热像仪、工业相机和高效的数据采集硬件,实现对工件表面缺陷的自动检测和分析。通过LabVIEW的强大功能,系统能够实时采集、处理和显示红外热波图像,有效提高了检测的精度和效率…

时域分析----移动平均滤波器介绍及其在金融应用示例

介绍 移动平均滤波器(Moving Average Filter)是一种基本但功能强大的信号处理技术,广泛应用于各种数据平滑和去噪任务中。其主要目的是通过对数据进行平均处理,减少随机波动和噪声,从而突出数据中的趋势和规律。移动平…

Win11任务栏当中对 STM32CubeMX 的堆叠问题

当打开多个 CubeMX 程序的时候,Win11 自动将其进行了堆叠,这时候就无法进行预览与打开。 问题分析:大部分ST的工具都是基于 JDK 来进行开发的,Win11 将其识别成了同一个 Binary 但是实际上他们并不是同一个,通过配置…

数据治理项目中,数据运营团队如何搭建能提升数据应用效果?

引言:在数据治理项目中,数据运营团队的搭建对于提升数据应用效果具有关键作用。以下是一些具体的步骤和策略,用于构建高效的数据运营团队以优化数据应用效果: 一、明确团队目标和职责 确定数据应用目标:首先&#xf…

【接口自动化_06课_Pytest+Excel+Allure完整框架集成】

一、logging在接口自动化里的应用 1、设置日志的配置,并收集日志文件 日志的设置需要在pytest.ini文件里设置。这个里面尽量不要有中文 2、debug日志的打印 pytest.ini文件的开关一定得是true才能在控制台打印日志 import allure import pytest from P06_PytestFr…

JavaScript(9) ----this指向问题,bind,call,apply等方法

目录 this指向问题 全局函数调用: 对象方法调用: 构造函数调用: 事件处理: 箭头函数: setTimeout和setInterval 7.使用call、apply或bind call 方法 apply 方法 bind 方法 总结 this指向问题 全局函数调用…

基于conda包的环境创建、激活、管理与删除

Anaconda是一个免费、易于安装的包管理器、环境管理器和 Python 发行版,支持平台包括Windows、macOS 和 Linux。下载安装地址:Download Anaconda Distribution | Anaconda 很多不同的项目可能需要使用不同的环境。例如某个项目需要使用pytorch1.6&#x…

STM32MP135裸机编程:支持内存非对齐访问

0 前言 使用stm32官方可视化初始化代码生成工具STM32CubeMX生成的工程GCC编译选项默认不支持非对齐访问,在我们进行非对齐的访问时就会进入数据异常中断DAbt中。为了解决这一问题,我们需要在GCC编译选项中加上一处配置。 1 操作方法 右键STM32CubeIDE…