写点什么

【高并发】线程的生命周期其实没有我们想象的那么简单!!

作者:冰河
  • 2022 年 6 月 08 日
  • 本文字数:2692 字

    阅读完需:约 9 分钟

【高并发】线程的生命周期其实没有我们想象的那么简单!!

大家好,我是冰河~~


在【高并发专题】中的《高并发之——线程与多线程》一文中,我们简单介绍了线程的生命周期和线程的几个重要状态,并以代码的形式实现了线程是如何进入各个状态的。今天,我们就结合操作系统线程和编程语言线程再次深入探讨线程的生命周期问题,线程的生命周期其实没有我们想象的那么简单!!


理解线程的生命周期本质上理解了生命周期中各个节点的状态转换机制就可以了。


接下来,我们分别就通用线程生命周期和 Java 语言的线程生命周期分别进行详细说明。

通用的线程生命周期

通用的线程生命周期总体上可以分为五个状态:初始状态、可运行状态、运行状态、休眠状态和终止状态。


我们可以简单的使用下图来表示这五种状态。


初始状态

线程已经被创建,但是不允许分配 CPU 执行。需要注意的是:这个状态属于编程语言特有,这里指的线程已经被创建,仅仅指在编程语言中被创建,在操作系统中,并没有创建真正的线程。

可运行状态

线程可以分配 CPU 执行。此时,操作系统中的线程被成功创建,可以分配 CPU 执行。

运行状态

当操作系统中存在空闲的 CPU,操作系统会将这个空闲的 CPU 分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态

休眠状态

运行状态的线程调用一个阻塞的 API(例如,以阻塞的方式读文件)或者等待某个事件(例如,等待某个条件变量等),线程的状态就会转换到休眠状态。此时线程会释放 CPU 资源,休眠状态的线程没有机会获得 CPU 的使用权。 一旦等待的条件出现,线程就会从休眠状态转换到可运行状态。

终止状态

线程执行完毕或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,这也意味着线程的生命周期结束了。


以上就是通用的线程生命周期,下面,我们再看对比看下 Java 语言中的线程生命周期。

Java 中的线程生命周期

Java 中的线程生命周期总共可以分为六种状态:初始化状态(NEW)、可运行/运行状态(RUNNABLE)、阻塞状态(BLOCKED)、无时限等待状态(WAITING)、有时限等待状态(TIMED_WAITING)、终止状态(TERMINATED)。


需要大家重点理解的是:虽然 Java 语言中线程的状态比较多,但是,其实在操作系统层面,Java 线程中的阻塞状态(BLOCKED)、无时限等待状态(WAITING)、有时限等待状态(TIMED_WAITING)都是一种状态,即通用线程生命周期中的休眠状态。也就是说,只要 Java 中的线程处于这三种状态时,那么,这个线程就没有 CPU 的使用权。


理解了这些之后,我们就可以使用下面的图来简单的表示 Java 中线程的生命周期。



我们也可以这样理解阻塞状态(BLOCKED)、无时限等待状态(WAITING)、有时限等待状态(TIMED_WAITING),它们是导致线程休眠的三种原因!


接下来,我们就看看 Java 线程中的状态是如何转化的。

RUNNABLE 与 BLOCKED 的状态转换

只有一种场景会触发这种转换,就是线程等待 synchronized 隐式锁。synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他的线程则需要等待。此时,等待的线程就会从 RUNNABLE 状态转换到 BLOCKED 状态。当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 状态转换到 RUNNABLE 状态。


这里,需要大家注意:线程调用阻塞 API 时,在操作系统层面,线程会转换到休眠状态。但是在 JVM 中,Java 线程的状态不会发生变化,也就是说,Java 线程的状态仍然是 RUNNABLE 状态。JVM 并不关心操作系统调度相关的状态,在 JVM 角度来看,等待 CPU 使用权(操作系统中的线程处于可执行状态)和等待 IO 操作(操作系统中的线程处于休眠状态)没有区别,都是在等待某个资源,所以,将其都归入了 RUNNABLE 状态。


我们平时所说的 Java 在调用阻塞 API 时,线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。

RUNNABLE 与 WAITING 状态转换

线程从 RUNNABLE 状态转换成 WAITING 状态总体上有三种场景。


场景一


获得 synchronized 隐式锁的线程,调用无参的 Object.wait()方法。此时的线程会从 RUNNABLE 状态转换成 WAITING 状态。


场景二


调用无参数的 Thread.join()方法。其中 join()方法是一种线程的同步方法。例如,在 threadA 线程中调用 threadB 线程的 join()方法,则 threadA 线程会等待 threadB 线程执行完。而 threadA 线程在等待 threadB 线程执行的过程中,其状态会从 RUNNABLE 转换到 WAITING。当 threadB 执行完毕,threadA 线程的状态则会从 WAITING 状态转换成 RUNNABLE 状态。


场景三


调用 LockSupport.park()方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换成 WAITING。调用 LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。

RUNNABLE 与 TIMED_WAITING 状态转换

总体上可以分为五种场景。


场景一


调用带超时参数的 Thread.sleep(long millis)方法;


场景二


获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout)参数;


场景三


调用带超时参数的 Thread.join(long millis)方法;


场景四


调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline)方法;


场景五


调用带超时参数的 LockSuppor.parkUntil(long deadline)方法。

从 NEW 到 RUNNABLE 状态

Java 刚创建出来的 Thread 对象就是 NEW 状态,创建 Thread 对象主要有两种方法,一种是继承 Thread 对象,重写 run()方法;另一种是实现 Runnable 接口,重写 run()方法。


注意:这里说的是创建 Thread 对象的方法,而不是创建线程的方法,创建线程的方法包含创建 Thread 对象的方法。


继承 Thread 对象


public class ChildThread extends Thread{    @Override    public void run(){        //线程中需要执行的逻辑    }}//创建线程对象ChildThread childThread = new ChildThread();
复制代码


实现 Runnable 接口


public class ChildRunnable implements Runnable{    @Override    public void run(){        //线程中需要执行的逻辑    }}//创建线程对象Thread childThread = new Thread(new ChildRunnable());
复制代码


处于 NEW 状态的线程不会被操作系统调度,因此也就不会执行。Java 中的线程要执行,就需要转换到 RUNNABLE 状态。从 NEW 状态转换到 RUNNABLE 状态,只需要调用线程对象的 start()方法即可。


//创建线程对象Thread childThread = new Thread(new ChildRunnable());//调用start()方法使线程从NEW状态转换到RUNNABLE状态childThread.start();
复制代码

RUNNABLE 到 TERMINATED 状态

线程执行完 run()方法后,或者执行 run()方法的时候抛出异常,都会终止,此时为 TERMINATED 状态。如果我们需要中断 run()方法,可以调用 interrupt()方法。

写在最后

如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。


最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。



好了,今天就到这儿吧,我是冰河,我们下期见~~

发布于: 2022 年 06 月 08 日阅读数: 29
用户头像

冰河

关注

公众号:冰河技术,专注写硬核技术专栏。 2020.05.29 加入

互联网资深技术专家,《深入理解分布式事务:原理与实战》,《海量数据处理与大数据技术实战》和《MySQL技术大全:开发、优化与运维实战》作者,mykit-data与mykit-transaction-message框架作者。【冰河技术】作者。

评论

发布
暂无评论
【高并发】线程的生命周期其实没有我们想象的那么简单!!_并发编程_冰河_InfoQ写作社区