【多线程系列-03】深入理解java中线程的生命周期,任务调度

news2024/12/24 20:27:15

多线程系列整体栏目


内容链接地址
【一】深入理解进程、线程和CPU之间的关系https://blog.csdn.net/zhenghuishengq/article/details/131714191
【二】java创建线程的方式到底有几种?(详解)https://blog.csdn.net/zhenghuishengq/article/details/127968166
【三】深入理解java中线程的生命周期,任务调度https://blog.csdn.net/zhenghuishengq/article/details/131755387

深入理解java中线程的生命周期,任务调度

  • 一,深入理解java中线程的生命周期,任务调度
    • 1,线程的生命周期
      • 1.1,线程的生命状态
      • 1.2,yield状态
    • 2,线程的调度
      • 2.1,协同式线程调度
      • 2.2,抢占式线程调度
    • 3,java中线程调度的实现方式
      • 3.1,内核线程实现
      • 3.2,用户线程的实现
      • 3.3,混合线程的实现
      • 3.4,java线程调度是抢占式原因
    • 4,守护线程
    • 5,协程
      • 5.1,协程的概念
      • 5.2,纤程的使用

一,深入理解java中线程的生命周期,任务调度

前一篇谈了线程的创建方式,接下来这篇深入的了解java中的线程

1,线程的生命周期

1.1,线程的生命状态

线程生命周期整体结构如下图所示,总共可以归纳为六种状态,分别是:初始状态,运行状态,等待状态,超时等待状态,阻塞状态和终止状态

在这里插入图片描述

1,首先是初始状态,此时实例化了一个线程,就是在堆内存中创建一个Thread实例,此时还没有调用start方法

Thread thread = new Thread();

2,在调用start方法之后,该线程会进入运行状态,但是由于线程的执行需要通过cpu的调度,因此在cpu没有轮换到该线程执行的时候,会处于一个就绪状态,当cpu时间片轮询到该线程时,则是处于一个运行中的状态。

3,在运行中,可能会遇到等待状态,这些等待的线程没有时间限制,需要其他线程唤醒

Object.wait();    <==>    Object.notify();
Thread.join();    
LockSupport.park();  <==>    LockSupport.unpark(Thread);

4,除了上面的这种等待状态,还有一种与之类似的超时等待 状态,这些等待的线程到达一定的时间自动被唤醒

Thread.sleep(long time);
Object.wait(long time);     <==>    Object.notify();
Thread.join(long time);    
LockSupport.parkNanos(long time);  <==>   LockSupport.unpark(Thread);
LockSupport.parkUntil(long time);

超时等待和等待的区别在于:等待是由于某个条件不满足而一直等待,超时等待是即使条件不满足,但是到一定的时间之后,还是会从等待状态变为运行状态

5,同时也存在一种block阻塞状态,就是平常时开发中用到的一些隐私锁操作,比如Synchronized ,这样就可以保证只有一个线程可以继续执行,其他的只有等这个线程释放锁之后,才能抢锁,再继续往下执行。而像Lock这种显示锁,其内部的线程时处于等待状态,并且其底层是通过CLH同步等待队列完成的

public static Synchronized void test();  //其他线程处于阻塞状态 
LockSupport.park();			//其他线程处于等待状态

6,在线程执行完之后,就会进入一个 TERMINATED终止状态

1.2,yield状态

除了以上六种主要的状态之外,还存在一个 yield 状态,该状态主要作用是礼让出cpu的执行权,让当前线程的状态从运行中的状态变为一个就绪状态。

在concurrentHashMap的initTable 方法中,就用到了这个线程礼让,这是因为 ConcurrentHashMap 中可能被多个线程同时初始化 table,但是其 实这个时候只允许一个线程进行初始化操作,其他的线程就需要被阻塞或等待, 但是初始化操作其实很快,为了避免阻塞或者等待这些操作 引发的上下文切换等等开销,就让其他不执行初始化操作的线程干脆执行 yield() 方法,以让出 CPU 执行权,让执行初始化操作的线程可以更快的执行完成

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

2,线程的调度

线程调度是指系统为线程分配 CPU 使用权的过程,线程中的调度主要有两种,一种是协同式线程调度,一种是抢占式线程调度

2.1,协同式线程调度

协同式线程调度指的是当前线程主要起协助作用,就是自身的控制是否停止当前cpu的调度。比如说当前线程在执行完任务之后,主动地的释放cpu的使用权,进而让系统去执行其他的线程。这样的好处是实现比较简单,并且不会出现并发的问题,但是坏处也比较明显,就是由于是串行执行,所有当一个线程出现问题的时候,后面的线程全部会跟着阻塞,导致整个程序阻塞

2.2,抢占式线程调度

而使用抢占式线程调度的多线程系统,每个线程执行的时间都会由系统进行决定,并且其时间线程不可控,比如说一个cpu对应10个线程,每个线程执行10s,那么cpu就会通过不断地切换执行线程,这样子就解决了上面的因一个线程而导致整个进程阻塞的问题。

java中使用的线程调度方式就是抢占式线程调度。 下文中会通过线程的调度方式说明为啥是抢占式而不是协同式

3,java中线程调度的实现方式

任何语言实现线程调度的方式总共有三种,分别是内核线程实现,用户线程实现,混合实现

3.1,内核线程实现

在操作系统内部已经实现了线程的实体以及所有的方法,内核态就是操作系统的核心,类似于人的大脑,负责整个操作系统的任务调度。

使用内核态实现线程的方式,就是通过内核控制操作系统线程调度器,让用户的创建的线程和操作系统的线程实现1:1的关系,就如java中就是使用的内核线程的方式实现的,在用户态new Thread在调用start之后,就会在操作系统中开启一个与之对应的线程,由在操作系统中实现了这些系统的调度等,因此不需要再语言层面进行控制,只需要将用户的应用代码交给操作系统即可。

这种方式的优点很明显,只需要进线程之间的映射,其他的任务调度这些交给操作系统即可;缺点也很明显,比如说一些线程的创建,线程的同步等,这些操作都是在用户态写的代码,因此需要通过系统调度来完成,那么就会产生上下文的切换,需要来回的从用户态切换到内核态,代价相对而言是比较高的。

如在java中这些线程的方法,其最终调用的还是native本地方法,就是直接操作本地的内核态,在通过内核态分发指令给操作系统,通过操作系统来完成以下的命令。

public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
public static native Thread currentThread();
private native boolean isInterrupted(boolean ClearInterrupted);
....

在java中每创建一个线程并且调用一个start方法,那么操作系统就得开启一个与之对于的线程,受硬件和操作系统之间的影响,线程的数量是有限的,因此在实际开发中,最好控制好线程的数量,如使用线程池来创建和管理线程。

3.2,用户线程的实现

由于通过内核方式实现这个线程的调度会产生大量的上下文切换,因此后面就有了用户线程的实现线程的调度,这样用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助,非常快速且低消耗的方式来支持更大的线程数量。这种线程可以实现操作系统线程和用户线程1:N的关系

这中方式的优点很明显,就是不需要上下文的切换,并且也不需要内核的支援;但是缺点也很明显,在用户态中要实现所有的关于线程的创建、销毁、切换和调度等的问题,并且没有内核的支援,所有关于线程在操作系统的方法都要在用户态的语言库中要实现一遍,相对而言比较复杂。

在现在如日中天的go语言以及其他支持高并发的语言中,已经是通过用户态的方式实现了线程的调度了。

3.3,混合线程的实现

上面的两种实现是纯粹的通过用户态或者内核态来实现线程调度的,因此在这两种基础上引入了混合线程的实现,就是让用户线程和内核线程同时使用,比如说让N个用户态的线程对应M个操作系统的线程,这样就解决了即可以使用操作系统中对线程的调度的一些方法,实现用户线程和操作系统线程的映射,也可以通过用户线程之间的直接切换,从而减少上下文的切换,让用户态的线程和操作系统的线程对应的关系是K:1

这样的缺点也有,就是要实现用户线程的切换以及用户线程所对应的那一个操作系统的线程。

3.4,java线程调度是抢占式原因

由于java采用的系统调度方式是内核线程的方式,因此java的线程和操作系统的线程时1对1的关系,也就是说具体的执行在java语言层面并不能够控制,因为完全是交给了操作系统去执行,所以在java中并不能够过控制住线程的优先级,jvm虚拟机也干涉不了操作系统内部是如何进行系统调度的,所以java线程并不是协同调度,而是抢占式调度

4,守护线程

在执行一个main主线程的代码之后,然后将其存在的线程全部打印

/**
 * @Author: zhenghuisheng
 * @Date: 2023/7/11 13:50
 * 单线程总统计
 */
public class ThreadCount {
    public static void main(String[] args) {
        // 获取线程管理bean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 获取线程和线程堆栈信息
        ThreadInfo[] threadInfo = threadMXBean.dumpAllThreads(false, false);
        for (int i = 0; i < threadInfo.length; i++) {
            ThreadInfo ti = threadInfo[i];
            //打印线程id
            System.out.println("线程id为:" + ti.getThreadId() + "线程名称为:" + ti.getThreadName());
        }
    }
}

其结果如下,除了打印这个main主线程之外,还存在其他的五个协助的线程,这几个线程就被称为守护线程,主要是在后台做调度和支持型工作。如负责垃圾回收的线程,就被称为守护线程。

线程id为:6线程名称为:Monitor Ctrl-Break      //监控中断信号的
线程id为:5线程名称为:Attach Listener 		//监听内存dump,类信息统计,获取系统属性等
线程id为:4线程名称为:Signal Dispatcher		//分发处理发送给JVM信号的线程
线程id为:3线程名称为:Finalizer				//调用finalize方法的线程
线程id为:2线程名称为:Reference Handler		//清除Reference线程
线程id为:1线程名称为:main	

在java语言中,除了上面的这些基本的守护线程之外,还可以通过这个设置daemon参数来讲普通线程修改成守护线程

//创建一个线程
Thread thread = new Thread();
//设置成守护线程
thread.setDaemon(true);

当时守护线程主要还是用来支持用户线程的,也就说一个线程开启的时候,同时也会开启多个守护线程,但是线程执行完毕之后,那么守护线程也会停止,所以在java中,并不推荐在自定义方法中调用这个finalize()方法,其一是会产生stw,其二就是这个finalize是守护线程的一个方法,如果此时刚好用户线程执行完毕,那么这个守护线程也会退出,就不会执行这个gc的调用了。

可以通过守护线程实现的应用主要有:内存清理、接收外部信号等。守护线程的主要作用是守护整个内存资源的回收和调度。

5,协程

5.1,协程的概念

虽然说如今java主流的线程调度方式还是内核调度,但是随着分布式以及微服务的兴起,通过内核线程调度会显得稍微吃力。比如说用户的一个请求,需要经过几个服务的链路,这样就可能出现响应时间慢,并发量大的问题,而如果这还使用内核调度的方式,即用户线程有多少个操作系统就得开启多少个线程,那么就需要在操作系统中创建大量的线程,而操作系统的线程数是有限的,那么能支持的并发数肯定是有限的,响应时间也会相对较慢。

而在go语言中,天然的高并发也是他的优势,相对于java,他的高并发的响应速率以及执行的线程数远远是操作java的,go内部采用的是用户态的方式实现线程的调度,因此java在受到多重因素的压力下,在 jdk19中也引入了这种虚拟线程,被称为"纤程",从而解决内核态带来的上下文切换导致的资源损耗问题,线程数量有限的问题以及响应速度慢的问题等。

纤程在java中,是一个轻量级的线程。如自定义创建一个线程,那么其线程需要的空间为1m,假设2000个线程,那么就需要2G的内存;但是在协程中,一个线程所占用的内存大小只需要几百个字节,所以一个纤程所占用的空间远远小于线程的空间。因此纤程可以处理的线程量就是原来的几千倍

纤程的缺点在于需要再用户态实现所有的线程调度算法,从而不依赖与操作系统。因此适合使用纤程的场景主要是:大并发,高io。高io指的是io密集型,就是大量的网络交互和磁盘交互,纤程只解决了规模数量的局限性,并没有解决速率慢的问题,因此并不适合cpu密集型。

因在jdk19中,引入了一个 Quasar 的纤程库,通过字节码注入的方式,在字节码 层面对当前被调用函数中的所有局部变量进行保存和恢复。这种不依赖 Java 虚 拟机的现场保护虽然能够工作,但也会影响性能。

5.2,纤程的使用

首先需要安装jdk19的版本,随后在pom文件中引入以下的依赖

<dependency> 
    <groupId>co.paralleluniverse</groupId>
    <artifactId>quasar-core</artifactId >
    <version>0.7.9 </version>
</dependency >

创建纤程的方式如下,Fiber类就是纤程相关的类,和java中普通线程的Thread一样

Fiber fiber = new Fiber(
	@Override
    public void run() throws Exception{
        ...
    }
);
fiber.start();

在运行时,需要添加对应的vm虚拟机参数,从而实现java的代理地址

-javaagent:D:\Maven\repository\co\paralleluniverse\quasar-core\0.7.9\quasarcore-0.7.9.jar

随着纤程的完善,通过 Executors.newVirtualThreadPerTaskExecutor() 提供了虚拟线程池功能,他是基于用户线程模式实现的,JDK 的调度程序 不直接将虚拟线程分配给处理器,而是将虚拟线程分配给实际线程,是一个 M: N 调度,具体的调度程序由已有的 ForkJoinPool 提供支持。

而在目前的版本中,还处于测试版本,并不推荐在开发中使用,所以目前为止了解即可,以后流行了再深入。

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

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

相关文章

基于树莓派实现的IO-Link 项目

IO-Link 协议 &#xff08;IEC 61131-9&#xff09; 是从传感器或执行器到 IO-Link 主站的串行半双工点对点连接。目前IO-Link 的硬应已经越来越普及。国外产品以巴鲁夫为代表。如何开发IO-link 产品&#xff1f;可以参考国外的一些开源项目。 国外有人开发了开发一个IO-Link主…

soundfile torchaudio 读取音频文件

soundfile 和 torchaudio 读取音频文件后的数据格式不同&#xff0c;前者是numpy&#xff0c;后者是tensor。前者读取后可以直接用于一些python的基础函数输入&#xff0c;后者用于pytorch的一些函数的应用。两者互换用途时候需要进行格式转换。 import soundfile as sf impor…

智能指针使用及详细解析

文章目录 智能指针概念为什么使用智能指针智能指针使用智能指针的常用函数get() 获取智能指针托管的指针地址.reset() 重置智能指针托管的内存地址&#xff0c;如果地址不一致&#xff0c;原来的会被析构掉 auto_ptrunique_ptrshared_ptr**shared_ptr的原理**引用计数的使用构造…

Gradle 构建工具 #5 又冲突了!如何理解依赖冲突与版本决议?

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。 Gradle 作为官方主推的构建系统&#xff0c;目前已经深度应用于 Android 的多个技术体系中&#xff0c;例如组件化开发、产物构建、单元测试等…

STM32(HAL库)驱动SHT30温湿度传感器通过串口进行打印

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 软件IIC引脚配置 2.3 串口外设配置 2.4 项目生成 3、KEIL端程序整合 3.1 串口重映射 3.2 SHT30驱动添加 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机通过HAL库…

Spring Batch之读数据库——JdbcCursorItemReader之自定义RowMapper(三十七)

一、自定义RowMapper 详情参考我的另一篇博客&#xff1a; Spring Batch之读数据库——JdbcCursorItemReader&#xff08;三十五&#xff09;_人……杰的博客-CSDN博客 二、项目实例 1.项目框架 2.代码实现 BatchMain.java: package com.xj.demo28;import org.springfram…

代码随想录第27天 | 455.分发饼干 ● 376. 摆动序列 ● 53. 最大子序和

455.分发饼干 /*** param {number[]} g* param {number[]} s* return {number}*/ var findContentChildren function(g, s) {let a0let b0let count0g.sort((a,b)>a-b)s.sort((a,b)>a-b)while(a<g.length&&b<s.length){if(s[b]>g[a]){countba}else{b}…

STM32(HAL库)软件IIC驱动OLED

目录 1、简介 2、CubeMX初始化配置 2.1 基础配置 2.1.1 SYS配置 2.1.2 RCC配置 2.2 软件IIC引脚配置 2.3 项目生成 3、KEIL端程序整合 3.1 OLED驱动添加 3.3 主函数代 3.4 效果展示 1、简介 本文通过STM32F103C8T6单片机&#xff08;HAL库&#xff09;通过软件IIC方式…

java linux服务器环境搭建

安装 jdk 下载jdk: wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24http%3A%2F%2Fwww.oracle.com%2F; oraclelicenseaccept-securebackup-cookie" "http://download.oracle.com/otn-pub/java/jdk/8u141-b15/336fa29ff2bb4ef291e347e091f7f…

Kubespray v2.22.1 在线部署 kubernetes v1.26.5 集群

文章目录 1. 介绍2. 预备条件3. 配置 hostname4. yum5. 下载介质5.1 git 下载5.2 下载 kubespray v2.22.1 6. 编写 inventory.ini7. 配置互信8. 安装 ansible9. 关闭防火墙10. 安装 docker11. 配置内核参数12. 启动容器 kubespray13. 部署14. 配置连接集群 1. 介绍 kubespray​…

Ubuntu18.04 安装vscode 配置C#编译器

环境&#xff1a; ubuntu 18.04 依赖库&#xff1a; SDK .net-7 安装对象&#xff1a; vscode 在终端&#xff1a; ./dotnet-install.sh --channel 7.0 遇见如下提示&#xff1a; dotnet&#xff1a;未找到命令 如下操作&#xff1a; 下载–解压–安装 wget https://pa…

Python 自学 day04 函数为参数传递, 匿名函数, 文件操作

1. 函数作为参数传递 &#xff08;类似C 函数指针&#xff09; def func(x):mm x(1,2);#print(f"mm的值是{mm}")return mmdef add(x,y): #加法return xy def reduce(x,y): # 减法return x-ydef ride(x,y): # 乘法return x*ydef divide(x,y): #带有小数点除法…

详解DDPG算法:解决对大量的超参数、随机重启、任务环境敏感问题,完成月球着陆器,双足机器人demo、以及超参数调优教学

0.demo展示 当我复现强化学习算法 DDPG 时,我发现论文中缺少必要的实现细节,例如:Gamma、噪声方差、最大训练步数等参数的取值。此外,在我调整参数,成功完成某次训练后,当我对随机种子进行修改,发现训练时长有很大变化,甚至有时候无法完成训练。更别提把在某个任务上 w…

1、linux中安装tomcat

1、创建目录 cd /opt ls mkdir tomcat 2、将文件拖入tomcat目录中 3、解压安装包 cd /opt/tomcat ls tar -zxvf apache-tomcat-8.5.59.tar.gz 4、启动tomcat cd /opt/tomcat/apache-tomcat-8.5.59/bin ./startup.sh 5、在linux中访问 http://localhost:8080/ 6、开放端口 …

为什么选择孟德尔随机化来写文章

为什么选择孟德尔随机化来写文章

【日常BUG】批量插入数据时报错: There is no setter for proerty named uptByd‘ in ‘class ...

前提&#xff1a;定义了自动填充策略&#xff0c;使用mybatis-plus 配置了属性自动注入 实体类上也定义了需要自动填充的字段。 但是在使用批量插入数据时报错&#xff1a; There is no setter for proerty named uptByd’ in class …&#xff0c; 排查过后发现是实体类中没…

QT QTableView添加CheckBox

需求&#xff1a;QTableView中指定列所有Item均为CheckBox项&#xff0c;要求CheckBox居中显示 显示效果如下&#xff1a;三种表现效果 实现方式&#xff1a; 系统控件QCheckBox 自绘制CheckBox CheckBox的图片 实现代码&#xff1a;(原理&#xff1a;利用委托自绘的方式)…

FasterViT实战:使用FasterViT实现图像分类任务(一)

文章目录 摘要安装包安装timm安装 grad-cam 数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集 摘要 论文翻译&#xff1a;https://blog.csdn.net/m0_47867638/article/details/131542132 官方源码&#xff1a;https://github.com/NVlabs/FasterViT 这是一篇来自英伟…

Flask_使用flask_marshmallow序列化数据

代码如下&#xff1a; from flask import Flask from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy from marshmallow import fieldsapp Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] "mysqlpymysql://root:12…

JavaFx 用户界面控件2——ListView

1.列表显示ListView 下面是一个JavaFX ListView的示例代码和使用方法&#xff1a; public class ListViewExample extends Application {Overridepublic void start(Stage primaryStage) {// 创建一个可观察的列表&#xff0c;用于存储ListView中的数据ObservableList<Str…