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

这篇具有很好参考价值的文章主要介绍了【多线程系列-03】深入理解java中线程的生命周期,任务调度。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

多线程系列整体栏目


内容 链接地址
【一】深入理解进程、线程和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,线程的生命状态

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

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

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 提供支持。

而在目前的版本中,还处于测试版本,并不推荐在开发中使用,所以目前为止了解即可,以后流行了再深入。文章来源地址https://www.toymoban.com/news/detail-581197.html

到了这里,关于【多线程系列-03】深入理解java中线程的生命周期,任务调度的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [Java]线程生命周期与线程通信

    【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://www.cnblogs.com/cnb-yuchen/p/18162522 出自【进步*于辰的博客】 线程生命周期与进程有诸多相似,所以我们很容易将两者关联理解并混淆,一些细节之处确有许多不同,因为线程调度与进程调度虽都由

    2024年04月27日
    浏览(35)
  • Java线程生命周期详解

    Java中的线程生命周期是多线程开发的核心概念。了解线程的生命周期以及它们如何进行状态转换对于编写有效且无错误的多线程程序至关重要。 Java线程主要有以下几个状态,这些状态定义在Thread.State枚举类中: 新建状态(New) :当我们创建一个新的线程实例时,线程就处

    2024年02月11日
    浏览(39)
  • JVM包含哪几部分?JVM内存模型?线程的生命周期? 对Spring AOP的理解?布隆过滤器

    JVM由三部分组成:类加载子系统、执行引擎、运行时数据区。 类加载子系统:可以根据指定的全限定名来载入类或接口。 执行引擎:负责执行那些包含在被载入类的方法中的指令。 运行时数据区:当程序运行时,JVM需要内存来存储许多内容,例如:字节码、对象、参数、返

    2024年02月16日
    浏览(47)
  • 【多线程系列-01】深入理解进程、线程和CPU之间的关系

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

    2024年02月16日
    浏览(69)
  • 软件测试03:软件工程和软件生命周期

    软件危机是指落后的软件生产方式无法满足迅速增长的计算机软件需求,从而导致软件开发与维护过程中出现一系列严重问题的现象。 基本软件危机对于计算机发展的阻碍,1968年,在联邦德国召开的国际会议,北大西洋公约组织的计算机科学家讨论软件危机问题。提出了 软

    2024年02月08日
    浏览(63)
  • 【天衍系列 03】深入理解Flink的Watermark:实时流处理的时间概念与乱序处理

    Watermark 是用于处理事件时间的一种机制,用于表示事件时间流的进展。在流处理中,由于事件到达的顺序和延迟,系统需要一种机制来衡量事件时间的进展,以便正确触发窗口操作等。Watermark 就是用来标记事件时间的进展情况的一种特殊数据元素。 Watermark 的生成方式通常是

    2024年02月20日
    浏览(44)
  • 线程生命周期、线程通讯

            有关线程生命周期就要看下面这张图,围绕这张图讲解它的方法的含义,和不同方法间的区别。    1、yield()方法     yield()让当前正在运行的线程回到就绪,以允许具有相同优先级的其他线程获得运行的机会。但是,实际中无法保证yield()达到让步的目的,因为,让

    2024年02月09日
    浏览(34)
  • 深入理解Java线程

    进程 程序由指令和数据组成,但程序要运行就要将指令加载进CPU以及数据加载进内存,并且在指令运行过程中可能还会用到磁盘、网络等设备。进程就是用来加载指令、管理内存和IO的。当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个进程。进程可以视

    2024年02月12日
    浏览(41)
  • 深入理解Java线程间通信

    合理的使用Java多线程可以更好地利用服务器资源。一般来讲,线程内部有自己私有的线程上下文,互不干扰。但是当我们需要多个线程之间相互协作的时候,就需要我们掌握Java线程的通信方式。本文将介绍Java线程之间的几种通信原理。 在Java中,锁的概念都是基于对象的,

    2024年02月09日
    浏览(48)
  • 深入理解Java多线程编程

            Java的多线程编程在现代应用程序中扮演着重要的角色。它可以提高应用程序的性能、并发处理能力和响应性。然而,多线程编程也带来了一些挑战,如线程安全、死锁和资源竞争等问题。本文将深入探讨Java多线程编程的基本概念和最佳实践。 1. 理解线程和进程

    2024年02月08日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包