zoukankan      html  css  js  c++  java
  • Java线程的生命周期概述版本

    Java线程的生命周期

    • New(新创建)
    • Runnable(可运行)
    • Blocked(被阻塞)
    • Waiting(等待)
    • Timed Waiting(计时等待)
    • Terminated(被终止)

    在我们程序编码中如果想要确定线程当前的状态,可以通过getState()方法来获取,同时我们需要注意任何线程在任何时刻都只能是处于一种状态。

    java api java.lang.Thread.State 这个枚举中给出了六种线程状态,分别是:

    线程状态 导致状态发生条件
    NEW(新建) 线程刚被创建,但是并未启动。 还没调用start方法 。
    Runnable(可运行) Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    ready: 线程**等待被线程调度器选中,得到时间片(获取cpu的执行权 ) **就能在cpu上面执行
    running: 就绪状态的线程在获得CPU时间片后变为运行中状态(running)
    是否获取到时间片就是这两者状态的区别
    阻塞(BLOCKED) 受阻塞并等待某个监视器锁的线程处于这种状态
    等待(WAITING) 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
    超时等待(TIMED_WAITING) 该状态不同于WAITING,它可以在指定的时间后自行返回。
    . 终止(TERMINATED) 表示该线程已经执行完毕。

    一、线程的状态图

    image.png

    二、状态详细说明

    1.初始状态(NEW)img

    • New表示线程被创建但尚未启动的状态:当我们用 new Thread()新建一个线程时,如果线程没有开始运行 start() 方法,那么线程也就没有开始执行 run()方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New变成 Runnable,进入到图中绿色的方框

    2.可运行状态(RUNNABLE)

    Runnable 状态的线程有可能正在执行,

    也有可能没有执行,正在等待被分配 CPU 资源。

    • 2.1就绪状态(RUNNABLE之READY)

      • 调用线程的start()方法,此线程进入就绪状态。
      • 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
      • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,这些线程也将进入就绪状态。
      • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
      • 锁池里的线程拿到对象锁后,进入就绪状态。
    • 2.2. 运行中状态(RUNNABLE之RUNNING)

    就绪状态和运行状态可以相互转换,比如 正在执行的线程时间片没了,此时调度器把cpu的时间片给了其他线程。

    正在执行的线程也可以自动调用yield方法主动放弃时间片,将cpu的执行权给别的线程

    Snipaste_2021-09-23_17-26-39

    3.阻塞状态(BLOCKED)

    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(未获取monitor锁)时的状态。

    • 我们可以通过下面的图示看到,从 Runnable 状态进入到 Blocked 状态只有一种途径,那么就是当进入到 synchronized 代码块中时未能获得相应的 monitor 锁(关于 monitor 锁我们在之后专门来介绍,这里我们知道 synchronized 的实现都是基于 monitor 锁的)

    • 在右侧我们可以看到,有连接线从 Blocked 状态指向了 Runnable ,也只有一种情况,那么就是当线程获得 monitor 锁,此时线程就会进入 Runnable 状体中参与 CPU 资源的抢夺

      img

    4.等待(WAITING)

    ​ 对于 Waiting 状态的进入有三种情况,如下图中所示,分别为:

    • 当线程中调用了没有设置 Timeout 参数的 Object.wait() 方法
    • 当线程调用了没有设置 Timeout 参数的 Thread.join() 方法
    • 当线程调用了 LockSupport.park() 方法

    img

    关于 LockSupport.park() 方法,这里说一下,我们通过上面知道 Blocked 是针对 synchronized monitor 锁的,

    但是在 Java 中实际是有很多其他锁的:

    比如 ReentrantLock 等,在这些锁中,如果线程没有获取到锁则会直接进入 Waiting 状态,其实这种本质上它就是执行了

    LockSupport.park() 方法进入了Waiting 状态

    **Blocked **与 ****Waiting** 的区别**

    • Blocked 是在等待其他线程释放 monitor 锁 ,其他条件都满足了吧
    • Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll()

    5.超时等待(TIMED_WAITING)

    • 最后我们来说说这个 Timed Waiting 状态,它与 Waiting 状态非常相似,其中的区别只在于是否有时间的限制,在 Timed Waiting 状态时会等待超时,之后由系统唤醒,或者也可以提前被通知唤醒如 notify

    • 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

      img

      通过上述图我们可以看到在以下情况会让线程进入 Timed Waiting状态。

      • 线程执行了设置了时间参数的Thread.sleep(long millis) 方法;

      • 线程执行了设置了时间参数的 Object.wait(long timeout) 方法;

      • 线程执行了设置了时间参数的 Thread.join(long millis) 方法;

      • 线程执行了设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和 LockSupport.parkUntil(long deadline) 方法。

        通过这个我们可以进一步看到它与 waiting 状态的相同

    6.终止状态(TERMINATED)

    1. run() 方法执行完毕,线程正常退出。
    2. 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。
    3. 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常

    二:线程状态间转换

    上面我们讲了各自状态的特点和运行状态进入相应状态的情况 ,那么接下来我们将来分析各自状态之间的转换,其实主要就是 BlockedwaitingTimed Waiting 三种状态的转换 ,以及他们是如何进入下一状态最终进入 Runnable

    Blocked进入 Runnable

    • 想要从 Blocked 状态进入Runnable 状态,我们上面说过必须要线程获得 monitor 锁,但是如果想进入其他状态那么就相对比较特殊,因为它是没有超时机制的,也就是不会主动进入。

    如下图中紫色加粗表示线路:

    img

    Waiting 进入 Runnable

    • 只有当执行了LockSupport.unpark(),或者join 的线程运行结束,或者被中断时才可以进入 Runnable 状态。
    • 如下图标注

    img

    • 如果通过其他线程调用 notify()notifyAll()来唤醒它,则它会直接进入 Blocked 状态,这里大家可能会有疑问,不是应该直接进入 Runnable 吗?这里需要注意一点 ,因为唤醒 Waiting 线程的线程如果调用 notify()notifyAll(),要求必须首先持有该 monitor 锁,这也就是我们说的 wait()notify 必须在 synchronized 代码块中。
    • 所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。

    Timed Waiting 进入 Runnable

    • 同样在 Timed Waiting 中执行 notify()notifyAll()也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态。

    img

    • 但是对于 Timed Waiting 而言,它存在超时机制,也就是说如果超时时间到了那么就会系统自动直接拿到锁,或者当 join 的线程执行结束/调用了LockSupport.unpark()/被中断等情况都会直接进入 Runnable 状态,而不会经历 Blocked 状态

    img

    三: 从mointorObject 的角度上理解阻塞与等待的区别

    什么是monitor?

    monitor可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象

    与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁也就是通常说Synchronized的对象锁

    在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; // 记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;// 指向持有ObjectMonitor对象的线程
        _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }
    

    ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时(即访问同一临界区时)

    1. 首先这些线程会进入 _EntryList 集合(处于阻塞状态的线程会放到该列表中),当线程A获取到对象的monitor后,Monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex,这时其它线程就无法再获取到该mutex。线程A进入 _Owner区域并把monitor中的owner变量设置为当前线程A,同时monitor中的计数器count加1
    2. 若线程A调用 wait() 方法,释放当前它锁持有的mutex, owner变量恢复为null,count自减1,并且该线程会进入到WaitSet集合(等待集合)中,等待下一次被其他线程调用notify/notifyAll唤醒。;
    3. 若线程A执行完毕,也将释放monitor(mutex锁)并复位count的值,以便其他线程获取monitor(mutex锁)进入monitor

    总结一下:

    那些处于EntrySet和WaitSet中的线程均处于阻塞状态(OS级别我们叫做阻塞,Java不叫作阻塞 。。https://my.oschina.net/goldenshaw?tab=newest&catalogId=3277710 这边有详解讲解os线程状态跟Java线程状态的区别),

    阻塞操作是由操作系统来完成的,线程被阻塞后便会进入到内核调度状态。线程的阻塞和唤醒需要CPU从用户态转为核心态(也叫做内核陷入),频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。(在linux下是通过pthread_mutex_lock函数实现的)

    同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

    所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)

    即:当发生对Monitor的争用时,若owner线程能够在很短的时间内释放掉锁,则那些正在争用的线程就可以稍微等待一下(既所谓的自旋),在Owner线程释放锁之后,争用线程可能会立刻获取到锁,从而避免了系统阻塞。

    不过,当Owner运行的时间超过了临界值后,争用线程自旋一段时间后依然无法获取到锁,这时争用线程则会停止自旋而进入到阻塞状态。

    所以总体的思想是:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码来说有极大的性能提升。显然,自旋在多处理器(多核心)上才有意义 。

    img

    Java精通并发-自旋对于synchronized关键字的底层意义与价值分析以及互斥锁属性详解与Monitor对象特性解说【纯理论】


    总结

    最后我们说一下再看线程转换的过程中一定要注意两点:

    • 线程的状态是按照箭头方向来走的,比如线程从 New状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。

    • 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。

    • 所以一个线程只能有一次 NewTerminated状态,只有处于中间状态才可以相互转换。也就是这两个状态不会参与相互转化

    参考:阻塞状态和等待状态的区别

  • 相关阅读:
    Office 2007在安装过程中出错-解决办法
    Sql日期时间格式转换
    大型网站架构演变和知识体系
    作为一个web开发人员,哪些技术细节是在发布站点前你需要考虑到的
    Java 入门基础
    技术类面试、笔试题汇总
    怎样玩转千万级别的数据
    数据库性能优化:SQL索引
    最后一公里极速配送
    编辑-滴滴算法大赛算法解决过程
  • 原文地址:https://www.cnblogs.com/tangliMeiMei/p/15327773.html
Copyright © 2011-2022 走看看