Java 面试必考题之线程的生命周期,结合源码,透彻讲解!
写在开头
在前面的几篇博客里,我们学习了 Java 的多线程,包括线程的作用、创建方式、重要性等,那么今天我们就要正式踏入线程,去学习更加深层次的知识点了。
第一个需要学的就是线程的生命周期,也可以将之理解为线程的几种状态,以及互相之间的切换,这几乎是 Java 多线程的面试必考题,每一年都有大量的同学,因为这部分内容回答不够完美而错过高薪,今天我们结合源码,好好来聊一聊。
线程的生命周期
所谓线程的生命周期,就是从它诞生到消亡的整个过程,而不同的编程语言,对线程生命周期的封装是不同的,在 Java 中线程整个生命周期被分为了六种状态,我们下面就来一起学习一下。
线程的 6 种状态
对于 Java 中线程的状态划分,我们其实要从两个方面去看,一是 JVM 层面,这是我们程序运行的核心,另一层面是操作系统层面,这是我们 JVM 能够运行的核心。为了更直观的分析,build 哥列了一个对比图:
在操作系统层面,对于 RUNNABLE 状态拆分为(READY、RUNNING),那为什么在 JVM 层面没有分这么细致呢?
这是因为啊,在当下时分多任务操作系统架构下,线程的驱动是通过获取 CPU 时间片,而每个时间片的间隔非常之短(10-20ms),这就意味着一个线程在 cpu 上运行一次的时间在 0.01 秒,随后 CPU 执行权就会发生切换,在如此高频的切换下,JVM 就没必要去区分 READY 和 RUNNING 了。
在 Java 的源码中也可以看到,确实只分了 6 种状态:
【源码解析 1】
NEW(初始化状态)
我们通过 new 一个 Thread 对象,进行了初始化工作,这时的线程还没有被启动。
【代码示例 1】
我们通过 thread.getState()方法去获得当前线程所处在的状态,此时输出为 NEW。
RUNNABLE(可运行状态)
对于这种状态的描述,我们来看一下 Thread 源码中如何说的:
大致意思是线程处于 RUNNABLE 状态下,代表它可能正处于运行状态,或者正在等待 CPU 资源的分配。
那么我们怎样从 NEW 状态变为 RUNNABLE 呢?答案很简单,我们只需要调用 start()方法即可!
【代码示例 2】
BLOCKED(阻塞状态)
当线程线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。这时候只有等到锁被另外一个线程释放,重新获取锁后,阻塞状态解除!
WAITING(无限时等待)
当通过代码将线程转为 WAITING 状态后,这种状态不会自动切换为其他状态,是一种无限时状态,直到整个线程接收到了外界通知,去唤醒它,才会从 WAITING 转为 uRUNNABLE。调用下面这 3 个方法会使线程进入等待状态:
Object.wait()
:使当前线程处于等待状态直到另一个线程唤醒它;Thread.join()
:等待线程执行完毕,底层调用的是 Object 的 wait 方法;LockSupport.park()
:除非获得调用许可,否则禁用当前线程进行线程调度。
TIMED_WAITING(有限时等待)
与 WAITING 相比,TIMED_WAITING 是一种有限时的状态,可以通过设置等待时间,没有外界干扰的情况下,达到指定等待时间后,自动终止等待状态,转为 RUNNABLE 状态。调用如下方法会使线程进入超时等待状态:
Thread.sleep(long millis)
:使当前线程睡眠指定时间;Object.wait(long timeout)
:线程休眠指定时间,等待期间可以通过 notify()/notifyAll()唤醒;Thread.join(long millis)
:等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;LockSupport.parkNanos(long nanos)
: 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;LockSupport.parkUntil(long deadline)
:同上,也是禁止线程进行调度指定时间;
TERMINATED(终止状态)
线程正常执行结束,或者异常终止,会转变到 TERMINATED 状态。
线程状态的切换
上面的 6 种状态随着程序的运行,代码(方法)的执行,上下文的切换,也伴随着状态的转变。
NEW 到 RUNNABLE 状态
这一种转变比较好理解,我们通过 new,初始化一个 Thread 对象后,这时就是处于线程的 NEW 状态,此时线程是不会获取 CPU 时间片调度执行的,只有在调用了 start()方法后,线程彻底创建完成,进入 RUNNABLE 状态,等待操作系统调度执行!这种状态是 NEW -> RUNNABLE 的单向转变。
RUNNABLE 与 BLOCKED 的状态转变
synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,等待的线程会从 RUNNABLE 转变到 BLOCKED 状态,当等待的线程获得 synchronized 隐式锁时,就又会从 BLOCKED 转变到 RUNNABLE 状态。我们通过一段代码示例看一下:
【代码示例 3】
输出:
代码中在主线程中创建了 2 个线程,线程中都调用了同步方法,随后去启动线程,因为 CPU 的执行效率较高,还没阻塞已经完成的打印,所以大部分时间里会输出两线程均为 RUNNABLE 状态;
当 CPU 效率稍低时,就会呈现上述结果,thread1 启动后进入 RUNNABLE 状态,并且获得了同步方法,这是 thread2 启动后,调用的同步方法锁已经被占用,它作为等待的线程会从 RUNNABLE 转变到 BLOCKED 状态,待到 thread1 同步方法执行完毕,释放 synchronized 锁后,thread2 获得锁,从 BLOCKED 转为 RUNNABLE 状态。
RUNNABLE 与 WAITING 的状态转变
1、获得 synchronized 隐式锁的线程,调用无参数的 Object.wait() 方法,状态会从 RUNNABLE 转变到 WAITING;调用 Object.notify()、Object.notifyAll() 方法,线程可能从 WAITING 转变到 RUNNABLE 状态。
2、调用无参数的 Thread.join() 方法。join() 是一种线程同步方法,如有一线程对象 Thread t,当调用 t.join() 的时候,执行代码的线程的状态会从 RUNNABLE 转变到 WAITING,等待 thread t 执行完。当线程 t 执行完,等待它的线程会从 WAITING 状态转变到 RUNNABLE 状态。
3、调用 LockSupport.park() 方法,线程的状态会从 RUNNABLE 转变到 WAITING;调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 转变为 RUNNABLE 状态。
RUNNABLE 与 TIMED_WAITING 的状态转变
这种与上面的很相似,只是在方法调用和参数上有细微差别,因为,TIMED_WAITING 和 WAITING 状态的区别,仅仅是调用的是超时参数的方法。转变方法在上文中已经提到了,这里以 sleep(time)为例,写一个测试案例:
【代码示例 4】
输出:
这里面我们启动 threa1 后,让主线程休眠了 1 秒,这时 thread1 获得同步方法后,方法内部执行了休眠 2 秒的操作,因此它处于 TIMED_WAITING 状态,而 thread2 正常运行结束,状态处于 TERMINATED(这个案例同样可以印证下面 RUNNABLE 到 TERMINATED 的转变)。
RUNNABLE 到 TERMINATED 状态
转变为 TERMINATED 状态,表明这个线程已经执行完毕,通常用如下几种情况:
线程执行完 run() 方法后,会自动转变到 TERMINATED 状态;
执行 run() 方法时异常抛出,也会导致线程终止;
Thread 类的 stop() 方法已经不建议使用。
总结
今天关于线程的 6 种状态就讲到这里啦,这是个重点知识点,希望大家能够铭记于心呀!
文章转载自:JavaBuild
评论