【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解

  • Post author:
  • Post category:java



目录


一、操作系统中进程和线程的状态


1.1 进程


1.1.1 进程的概念


1.1.2 进程的状态


1.1.3 进程调度流程图(五状态)


1.1.4 挂起状态


1.1.4 进程调度流程图(六状态和七状态)


1.1.5 睡眠状态


1.1.6 进程的诞生与消亡


1.2 线程


1.2.1 线程的概念


1.2.2 线程与进程的比较


1.2.3 线程的状态


1.2.4 线程调度流程图


1.2.5 线程的内核调度


1.2.6 线程的诞生与消亡


二、Java中的线程


2.1 Java 线程的状态


2.2 操作系统中的线程状态


2.3 Java线程的状态流转图


2.4 验证Java线程状态流转


2.4.1 IDEA验证Java线程状态流转


2.4.2 控制台如何查看Java线程状态




一、操作系统中进程和线程的状态




1.1


进程




1.1.1


进程的概念


我们可以简单理解为进程就是执行中的程序。进程是程序运行资源分配的最小单位。



1.1.2


进程的状态


操作系统中进程有五种状态,分别是:


  • 新建状态(new)

    :进程正在被创建,尚未到就绪状态。

  • 就绪状态(ready)

    :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。

  • 运行状态(running)

    :进程正在处理器上上运行(单核CPU下任意时刻只会有一个进程处于运行状态)。

  • 阻塞状态(waiting)

    :又称为等待状态,进程正在等待某一事件而暂停运行,如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。

  • 结束状态(terminated)

    :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。



1.1.3


进程调度流程图(五状态)


上图就是经典的进程五状态模型。但事实上还存在被挂起的进程。

要说挂起、阻塞、睡眠难免让人想到进程生命周期中的阻塞态(等待状态),而挂起和睡眠却没有出现在上面的五状态模型图中,说明这三个其实在本质上区别并不那么大,但是既然称呼不同,应该就有不同的道理。事实上还存在被挂起的进程,所以在后面又拓展了进程状态模型,增加到了六状态和七状态模型图。



1.1.4


挂起状态


上面进程调度流程图中的三个基本状态(就绪态、运行态和阻塞态)为建立进程行为模型提供了一种系统方法,并指导操作系统的实现。

但是,可以证明往模型中增加其他状态也是合理的。下面考虑一个没有使用虚拟内存的系统,每次执行中的进程必须完全载入内存。因此,所有队列中的所有进程必须驻留在内存中。

对于I/O密集型的进程,一个进程进入等待(阻塞)状态后,处理器会转向处理另一个就绪的进程,但是由于处理器处理的速度相比I/O要快的多,以至于内存中所有进程都在等待I/O的情况很常见,所以可能会出现所有进程都处于阻塞状态的情况。导致处理器的效率低下。即使是多道程序设计,大多数时候处理器仍然可能处于空闲状态。


这种情况有两种解决办法:

  1. 一种解决办法是扩充内存适应更多的进程。有以下缺点:1.内存的价格。2.程序对内存空间需求的增长速度比内存价格下降的速度快。因此,更大的内存往往导致更大的进程,而不是更多的进程。
  2. 另一种解决方案是


    交换


    。包括把内存中某个进程的一部分或全部移到磁盘中。当内存中没有处于就绪状态的进程时,操作系统就把被阻塞的进程换出到磁盘中的

    挂起队列(suspend queue)

    ,即暂时保存从内存中被 “驱逐” 出来的阻塞进程的队列。操作系统在之后可以取出挂起队列中的另一个进程,或者接受一个新进程的请求,将其纳入内存运行。



交换(swapping)


是一个I/O操作,因而可能使问题更恶化。但是由于磁盘I/O一般是系统中最快的I/O(相对于磁带或者打印机I/O),所以交换通常会提高性能

基于上面描述的一种现实问题,利用交换这个I/O操作,就产生了


挂起


这样一个新的状态。



1.1.4


进程调度流程图(六状态和七状态)


现在有两种进程模型,一种是包含单挂起态的模型,一种是包含两个挂起态的模型。

在七状态模型中,新增了和挂起相关的两个概念,即

阻塞挂起状态、就绪挂起状态

  • 就绪态:进程在内存中并可以执行。
  • 阻塞态:进程在内存中并等待一个事件。
  • 阻塞/挂起态:进程在外存中并等待一个事件。
  • 就绪/挂起态:进程在外存中,但是只要被载入内存就可以执行。

与之前五个转换模型相比,比较重要的新转换如下:


  • 阻塞→阻塞/挂起

    :如果没有就绪进程,则至少一个阻塞进程被换出,为另一个没有阻塞的进程让出空间,被换出的阻塞进程就会进入到阻塞/挂起状态。而且如果操作系统确定当前正在运行的进程,或就绪进程为了维护基本的性能要求而需要更多的内存空间,那么,即使有可用的就绪态进程也可能出现这种转换。

  • 阻塞/挂起→就绪/挂起

    :如果等待的事件发生了,则处于阻塞/挂起状态的进程可以转换到就绪/挂起状态。注意,这要求操作系统必须能够得到挂起进程的状态信息。

  • 就绪/挂起→就绪

    :如果内存中没有就绪态进程,操作系统需要调入一个进程继续执行,这个时候操作系统就会将就绪/挂起状态转化为就绪状态,即将挂起进程转移回内存中去。此外,当处于就绪/挂起态的进程比处于就绪态的任何进程的优先级都要高时,也可以进行这种转换。这种情况的产生是由于操作系统设计者规定调入高优先级的进程比减少交换量更重要。

  • 就绪→就绪/挂起

    :通常,操作系统更倾向于挂起阻塞态进程而不是就绪态进程,因为就绪态进程可以立即执行,而阻塞态进程占用了内存空间但不能执行。但如果释放内存以得到足够空间的唯一方法是挂起一个就绪态进程,那么这种转换也是必需的。并且,如果操作系统确信高优先级的阻塞态进程很快将会就绪,那么它可能选择挂起一个低优先级的就绪态进程,而不是一个高优先级的阻塞态进程。

还需要考虑的几种其他转换有:


  • 新建->就绪挂起


    以及


    新建->就绪

    :当创建一个新进程时,该进程要么加入就绪队列,要么加入就绪/挂起队列。不论哪种情况,操作系统都必须建立一些表来管理进程,并为进程分配地址空间。操作系统可能更倾向于在初期执行这些辅助工作,这使得它可以维护大量的未阻塞的进程。通过这一策略,内存中经常会没有足够的足够的空间分配给新进程。因此使用了(新建->就绪/挂起)转换。另一方面,我们可以证明创建进程时适时(just-in-time)原理,即尽可能推迟创建进程以减少操作系统的开销,并在系统被阻塞态进程阻塞时允许操作系统执行进程创建任务。

  • 阻塞/挂起->阻塞

    :这种转化在设计中比较少见,如果一个进程没有准备好执行,并且不在内存中,调入它又有什么意义?但是考虑到下面的情况:一个进程终止,释放了一些内存空间,阻塞/挂起队列中有一个进程优先级比就绪/挂起队列中任何进程的优先级都要高,并且操作系统有理由相信阻塞进程的事件很快就会发射管,这时,把阻塞进程而不是就绪进程调入内存是合理的。

  • 运行->就绪/挂起

    :通常当分配给一个运行进程的CPU时间用完时,它将转换回就绪态。但是,如果由于位于阻塞/挂起队列中具有较高优先级的进程变得不再被阻塞,操作系统抢占这个进程,也可以直接把这个运行进程转换到就绪/挂起队列中,并释放一些内存空间。

  • 各种状态->退出

    :在典型情况下,一个进程在运行时终止,或者是因为它已经完成,或者是因为出现了一些错误条件。但是,在某些操作系统中,一个进程可以被创建它的进程终止,或者当父进程终止时终止。如果允许这样,则进程在任何状态时都可以转换到退出态。



通俗的说,挂起或者不挂起,不光要考虑为进程让出空间,不光要考虑是否就绪,还要考虑进程的优先级。

挂起的其他用途:到目前为止,挂起进程的概念与不在内存中的进程概念是等价的。一个不再内存中的进程,不论是否在等待一个事件,都不能立即执行,都需要再转换几个状态后才可以执行。


总结一下挂起进程的概念:

  1. 挂起进程不能立即执行。
  2. 挂起进程可能正在等待一个事件。如果确实是在等待某一个事件,那么阻塞条件不依赖于挂起条件,事件的发生不会使挂起进程立即被执行。
  3. 为阻止进程执行,可以通过代理把这个进程置于挂起状态,代理可以是进程自己,也可以是父进程或操作系统。
  4. 除非代理显示地命令系统进行状态转换,否则挂起进程无法从这个状态中转移。


下面一张表展示挂起进程的原因:



1.1.5


睡眠状态


在上一篇笔记中,讲过睡眠操作,但是我们发现睡眠状态并没有在七状态模型中出现,其实它是阻塞状态(等待状态)下的一种更细的分支。这样说的依据是进程由运行状态进入阻塞状态的原因,睡眠是进程通过代理(自己或父进程)主动引起的进程调度,并且这种阻塞状态恢复到就绪状态的时间是确定的。而狭义上的阻塞可以理解为一个被动的动作。


关于睡眠,有一篇博客是这样解释的:

  • 当一个进程获取资源比如获取最普通的锁失败后,可以有两种处理方式,1、自己睡眠,触发调度;2、忙等待,使用完自己的时间。所以从这里看,睡眠的确是一种主动的方式,且仅仅作为一种处理手段。当然睡眠不仅仅用于阻塞,更多的,我们可以在适当的时候设置让进程睡眠一定的时间,那么在这里就可以发现,睡眠之前我们已经预先规定了线程只能睡多长的时间,这段时间过后,比必须返回来工作。



1.1.6


进程的诞生与消亡



  • 进程的诞生

    • (1)fork函数:子进程拷贝父进程的数据(具体实现是读时共享,写时复制)
    • (2)vfork函数:子进程与父进程共享数据
    • vfork是一个过时的函数,虽然与fork相比有那么一点性能优势,但其带来一连串的坑并不那么好填,不建议使用,除非你对性能追求到极致。

  • 进程的消亡

    • 正常结束和异常终止;
    • 进程结束时的资源问题回收:linux系统设计时规定:每个进程结束时,系统会自动回收open但没有close的文件资源,malloc但没有free的资源等,但并不会回收进程本身占用的资源(即进程的尸体,主要包括进程本身的文件描述符对应的资源(task_struct结构体)和进程的栈空间),这需要由进程的父进程来完成回收(收尸)。

  • 僵尸进程

    • 在子进程消亡后,如果父进程没有结束,而且也不回收已结束的子进程(收尸),已经结束的子进程,就变成了僵尸进程。
    • 父进程可以使用wait或waitpid,显式地回收子进程(剩余待回收)的内存资源并且获取子进程退出状态。
    • 父进程结束时也会自动回收僵尸进程,但应避免这种不严谨的方式。

  • 孤儿进程

    • 子进程还在执行,而父进程先结束了,子进程就成为了孤儿进程,托管到系统了。
    • 此时子进程的父进程变为了系统的init进程(该进程PID为1),init进程会在孤儿进程结束后自动回收孤儿进程的资源。

  • 进程回收

    • 可以使用wait或waitpid,阻塞回收进程资源,阻塞回收具有明显的劣势,会导致阻塞的父进程不能干别的事情了。
    • Linux生产中更多的是采用信号机制,父进程注册信号,收到SIGCHLD信号才调用回收函数回收子进程资源,这样就不会导致父进程阻塞了。



1.2


线程




1.2.1


线程的概念


线程是CPU调度和分派的基本单位,线程早期也有轻量级进程之称。一个进程中可能包含多个线程。一个进程至少包含一个线程,否则没有存在的意义。多线程里不宜再创建子进程。在系统内核层面,进程与线程并无本质的不同。进程与线程最大的不同点是资源分配。



1.2.2


线程与进程的比较


  • 线程与进程都可以实现多任务。
  • 线程是CPU调度的基本单元,进程是系统资源分配的基本单元。
  • windows下进程和线程是泾渭分明,区别明显的。在Linux中它们有很多共同特性。
  • 在早期Linux的内核结构中:进程和线程的区别只是创建子进程和子线程时,是否设置为共享内存,二者在内核中的存储结构并无区别,系统调度的单位也是轻量级进程。2.6以后的Linux内核版本才将线程和进程完全独立开来。
  • 线程的状态改变只代表了CPU执行过程的改变,线程操作的资源仍然是进程的。除了CPU外,计算机内的软硬件资源的分配都是以进程为单位的。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在,而线程只是进程的一部分,与进程内的其他线程一起共享分配给该进程的所有资源。



1.2.3


线程的状态


同进程的实现原理类似,线程也可主要概括为五种状态(实际上Linux将线程状态细分为十几种):


  • 新建(


    new




    :由于不需要进行必要的内存复制等工作,新建线程要比新建进程更快。

  • 就绪(


    ready




  • 运行(


    running




  • 阻塞(


    waiting




  • 死亡(terminated)

    :线程死亡后,也需要回收处理。

基本和进程是一样的,线程调度的过程也可以直接参考进程的调度过程,两者都是一样的。



1.2.4


线程调度流程图


和进程的状态模型图基本是一样的。



1.2.5


线程的内核调度


多线程编程具有响应度高、资源共享、经济和多处理器体系结构的利用四个优点。用户线程是映射到内核线程池进行CPU调度的,映射关系模型包含有:

  • 多对一
  • 一对一
  • 多对多


内核调度图:

  • 这里为什么没有一对多?因为线程是CPU资源调度的最小单位,即:单线程在一个时间点上只能利用到一个核心(进行一个原子操作),一个原子操作不能再分开由不同核心执行。而多核CPU在执行单线程任务时,可能会切换多个核心轮流来执行这个任务(每个原子操作的CPU核心可能并不相同),例如在执行循环时,这次循环和下次循环可能并不是同一个核心来执行的(这跟你的系统有关,但可以看到单线程最多只能占用到 (1/CPU核心数) 的CPU资源(超线程CPU占用1/(CPU线程数))。
  • 而资源上,多线程调用同一资源时,X86架构可能会使用总线锁,对该资源进行锁定,保证原子操作执行完整不被打断。当操作完成时,会解锁并通知其他线程,我操作完了,你们可以来操作了(实际上,此方法效率很低,仅作为最后一道保险)。
  • 因此确定一个操作是原子操作时,没有必要浪费外围昂贵的开销再来给他加锁,原子操作本身就是一道互斥锁。互斥锁的目的,也正是将一系列操作变为原子操作。



1.2.6


线程的诞生与消亡



  • 线程标识(线程ID)

    • 进程ID在整个系统中是唯一的。
    • 线程ID(pthread_t类型)只在它所属的进程中有效。
    • pthread_t(Linux中为unsigned int类型,OS中为结构体指针,Windows中为handle句柄)用于声明线程ID。
    • 函数:pthread_self取得自身线程ID。

  • 创建线程

    • 使用函数pthread_create,线程创建后,就开始运行相关的线程函数。

  • 退出线程。

    • 线程执行完毕。可以return,不能exit(exit是退出进程)。
    • 使用函数pthread_exit,主动退出线程。主线程使用该函数时,进程并不会结束,而是等待其他线程结束。
    • 进程结束时,线程也结束(线程依赖于其所在的进程)。

  • 线程回收


    • 由于线程使用的资源是属于进程的,退出线程而进程仍然运行时资源并未完全释放,形成僵尸线程

    • pthread_join(tid)函数类似wait/waitpid函数,用于阻塞等待线程tid结束,调用它的线程一直等待到tid线程结束时,tid线程资源就被回收。
    • pthread_detach(tid)函数线程分离,让系统自动回收tid线程。
    • 设置线程属性进行回收。可按以下步骤回收:

      • pthread_attr_t attr;//线程创建前,定义线程属性
      • pthread_attr_init(&attr);//进行初始化线程属性
      • pthread_attr_getdetachsate(&attr,&status);//获取分离状态
      • pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置线程分离状态.
      • pthread_create(&tid, &attr,func,NULL);//创建线程
      • pthread_attr_destroy(&attr);//线程结束时,调用回收函数
    • 线程回收代码示例:
		void * func(void *p)
		{
		    printf("我是子线程\n");
		}
		int main(int argc, char *argv[])
		{
		    pthread_attr_t attr; //定义一个变量
		    pthread_t tid;
		    pthread_attr_init(&attr);//初始化
		    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置分离
		    pthread_create(&tid, &attr, func, NULL);//创建线程
		    sleep(1);//等1秒让子线程执行完
		    pthread_attr_destroy(&attr);//释放
		    return 0;
}



二、


Java


中的线程


这一章节我们主要讲的内容有:

  • Java 中对线程状态的定义,与操作系统线程状态的对比
  • Java线程状态的流转图
  • 如何自己验证状态的流转



2.1


Java


线程的状态


先来谈一谈Java 中线程的状态。在 java.lang.Thread.State 类是 Thread的内部枚举类,在里面定义了Java 线程的六个状态,注释信息也非常的详细。

public enum State {

    /**
     * Thread state for a thread which has not yet started.
     * 初始态,代表线程刚创建出来,但是还没有执行start()的状态
     */
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     * 
     * 运行态,代表线程正在运行或者等待操作系统资源,如CPU资源
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     * 
     * 阻塞态,代表线程正在等待一个监视器锁(即我们常说的synchronized)
     * 或者是在调用了Object.wait之后被notify()重新进入synchronized代码块
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     * 
     * 等待态,调用以下方法会进入等待状态:
     * 1. 调用不会超时的Object.wait()方法
     * 2. 调用不会超时的Thread.join()方法
     * 3. 调用不会超时的LockSupport.park()方法
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     * 
     * 超时等待态,在调用了以下方法后会进入超时等待状态
     * 1. Thread.sleep()方法后
     * 2. Object.wait(timeout)方法
     * 3. Thread.join(timeout)方法
     * 4. LockSupport.parkNanos(nanos)方法
     * 5. LockSupport.parkUntil(deadline)方法
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     * 
     * 终止态,代表线程已经执行完毕
     */
    TERMINATED;
}

关于上面JDK源码中对于BLOCKED状态的注释,这里有一点需要补充的,就是如果是线程调用了Object.wait(timeout)方法进入TIMED_WAITING状态之后,如果是因为超过指定时间,脱离TIMED_WAITING状态,如果接下去线程是要重新进入synchronize 代码块的话,也是会先进入等待队列,变成BLOCKED状态,然后请求监视器锁资源。


以上Java线程的六种状态分为正常状态3种:


  1. NEW(新建状态):

    Mythread thread=new MyThread();此时thread的状态,还没有调用 start()方法

  2. RUNNABLE(运行状态):

    thread.start();此时thread的状态,但是处于这个状态的线程并不一定真正执行,可能会存在在等待资源,因此网上也有人将这个状态分为

    就绪状态



    执行状态

    两种。(Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行状态”)

  3. TERMINATED(结束状态):

    线程执行完之后的状态,表示当前线程已经执行完毕


异常状态3种:


  1. BLOCKED(阻塞状态):

    通常出现在synchronized语句块中,代表着一个线程等待锁

  2. WAITING(等待状态):

    通常出现在Object.wait(),Thread.join(),LockSupport.park()语句前后,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)

  3. TIMED_WAITING(超时等待状态):

    当前线程的等待时间是有限制的,时间一到线程就会被自动唤醒,通常出现Thread.sleep(long),Object.wait(long),Thread.join(long),LockSupport.parkNanos(),LockSupport.parkUntil()



超时等待(TIMED_WAITING)和等待(WAITING)的区别?

等待是在阻塞队列中,超时等待是在阻塞队列外。



很多人会有疑问,WAITING状态和BLOCKED状态有什么不同?

其实两者没有什么本质的不同,都是阻塞状态。

只不过WAITING是通过执行一些方法来触发的阻塞。而BLOCKED是等待锁引发的阻塞,他们两个的区别只是被阻塞的原因或者说背景不同,但是本质都是阻塞。BLOCKED、WAITING、TIMED_WAITIN这些状态其实都算是阻塞。



2.2


操作系统中的线程状态


前面的章节已经讲过了,操作系统层面线程存在五类状态,状态的流转关系可以参考下面的这张图。

可以看到,Java 中所说的线程状态和操作系统层面的线程状态是不太一样的。


  • Java 中的 RUNNABLE 其实包含了OS中的RUNNING(运行态)和READY(就绪态)


  • Java 中的WAITING、TIMED_WAITING、BLOCKED其实是对OS中WAITING状态(阻塞态)的一个更细致的划分


在Thread.State源码中也写了这么一句话:


These states are virtual machine states which do not reflect any operating system thread states.这些状态只是线程在虚拟机中的状态,并不反映操作系统的线程状态。

对于这两个层面对比,你需要知道的是,

Java的线程状态是服务于监控的

。从这个角度来考虑的话,把底层OS中的RUNNING和READY状态映射上来也没多大意义,因此,统一成为RUNNABLE 状态是不错的选择,而对WAITING状态更细致的划分,也是出于这么一个考虑。

Java本身是没有线程的,线程都是操作系统提供的。Java只是对操作系统提供的线程进行一些封装,让用户能更方便的使用操作线程。


Java


线程只是操作系统线程在


JVM


中的一个映射而已,


Java


线程的状态并不代表操作系统中真实线程的状态



2.3 Java


线程的状态流转图


关于阻塞状态,这里还要多说几句话,我们上面说的,都是在JVM代码层面的实际线程状态。但是在一些书比如《码出高效》中,会把Java 线程的阻塞状态分为:

  • 同步阻塞:即锁被其他线程占用
  • 主动阻塞:指调用了Thread 的某些方法,主动让出CPU执行权,比如sleep()、join()等
  • 等待阻塞:执行了wait()系列方法



线程不同状态之间的转化是谁来实现的呢?是JVM吗?

并不是。JVM需要通过操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。



2.4


验证


Java


线程状态流转




2.4.1 IDEA


验证


Java


线程状态流转


这里演示一下,如何在IDEA 上面来验证上述的状态流转。有疑问或者有兴趣的读者可以按照同样的方法来验证。

我这里想要用代码验证下面的情况:

  • 就是如果是线程1调用了Object.wait(timeout)方法进入TIMED_WAITING状态之后,如果是因为超过指定时间,脱离TIMED_WAITING状态,如果接下去线程是要重新进入synchronize 代码块的话,也是会先进入等待队列,变成BLOCKED状态,然后请求监视器锁资源。
public class ThreadLifeTempTest {
    public static void main(String[] args) {
        Object object = new Object();
        new Thread(()->{
            synchronized (object) {
                try {
                    System.out.println("thread1 waiting");
                    // 调用wait会释放锁(这就给了Thread2机会来获取到object对象锁),等待10s,进入Timed_Waiting
                    // 10s 后会继续执行后续synchronized代码块中的代码,但是因为获取不到锁,就会进入Blocked,获取object的监视器锁
                    object.wait(10000);
                    System.out.println("thread1 after waiting");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread1").start();

        new Thread(()->{
            synchronized (object) {
                try {
                    // sleep也不会释放锁,所以Thread1 不会获取到锁
                    Thread.sleep(10000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread2").start();
    }
}

使用IDEA的RUN模式运行代码,然后点击左边的一个摄像头按钮(dump thread),查看各线程的状态。

在Thread 1 等待 10s中时,dump的结果:Thread 1和Thread 2都处于


TIMED_WAITING


状态:

"Thread2" #13 prio=5 os_prio=0 tid=0x0000000020196800 nid=0x65b8 waiting on condition [0x0000000020afe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at main.java.concurrent.thread.ThreadLifeTempTest.lambda$main$1(ThreadLifeTempTest.java:33)
    - locked <0x000000076b71c748> (a java.lang.Object)
    at main.java.concurrent.thread.ThreadLifeTempTest?Lambda$2/1096979270.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)
"Thread1" #12 prio=5 os_prio=0 tid=0x0000000020190800 nid=0x25fc in Object.wait() [0x00000000209ff000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076b71c748> (a java.lang.Object)
    at main.java.concurrent.thread.ThreadLifeTempTest.lambda$main$0(ThreadLifeTempTest.java:21)
    - locked <0x000000076b71c748> (a java.lang.Object)
    at main.java.concurrent.thread.ThreadLifeTempTest?Lambda$1/1324119927.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

在Thread 1 等待 10s之后,Thread 1重新进入


synchronize


代码块,进入等待队列,变成


BLOCKED


状态:

"Thread2" #13 prio=5 os_prio=0 tid=0x0000000020196800 nid=0x65b8 waiting on condition [0x0000000020afe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at main.java.concurrent.thread.ThreadLifeTempTest.lambda$main$1(ThreadLifeTempTest.java:33)
    - locked <0x000000076b71c748> (a java.lang.Object)
    at main.java.concurrent.thread.ThreadLifeTempTest?Lambda$2/1096979270.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)
"Thread1" #12 prio=5 os_prio=0 tid=0x0000000020190800 nid=0x25fc waiting for monitor entry [0x00000000209ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076b71c748> (a java.lang.Object)
    at main.java.concurrent.thread.ThreadLifeTempTest.lambda$main$0(ThreadLifeTempTest.java:21)
    - locked <0x000000076b71c748> (a java.lang.Object)
    at main.java.concurrent.thread.ThreadLifeTempTest?Lambda$1/1324119927.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)



2.4.2


控制台如何查看


Java


线程状态


步骤:

  1. 在控制台使用


    jps




    命令


    ,查看Java进程的服务,获得其PID
  2. 再使用jstack PID命令,查看这个进程中的线程运行状态,这个也是排查死锁的方法。


如何排查死锁的流程就是这个样子:使用jps获取PID,然后再用jstack查看这个PID中的线程的运行状态。



相关文章:


【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别



【并发基础】Java中线程的创建和运行以及相关源码分析



【并发基础】线程,进程,协程的详细解释



版权声明:本文为cy973071263原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。