天天看点

Java线程的生命周期概述版本

Java线程的生命周期

New(新创建)

Runnable(可运行)

Blocked(被阻塞)

Waiting(等待)

Timed Waiting(计时等待)

Terminated(被终止)

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

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

<col>

线程状态

导致状态发生条件

NEW(新建)

线程刚被创建,但是并未启动。 还没调用start方法 。

Runnable(可运行)

Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

ready: 线程**等待被线程调度器选中,得到时间片(获取cpu的执行权 ) **就能在cpu上面执行

running: 就绪状态的线程在获得CPU时间片后变为运行中状态(running)

是否获取到时间片就是这两者状态的区别

阻塞(BLOCKED):

受阻塞并等待某个监视器锁的线程处于这种状态

等待(WAITING):

进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)

超时等待(TIMED_WAITING):

该状态不同于WAITING,它可以在指定的时间后自行返回。

. 终止(TERMINATED):

表示该线程已经执行完毕。

一、线程的状态图

Java线程的生命周期概述版本

二、状态详细说明

1.初始状态(NEW)

Java线程的生命周期概述版本

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

2.可运行状态(RUNNABLE)

​<code>​Runnable​</code>​ 状态的线程有可能正在执行,

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

2.1就绪状态(RUNNABLE之READY)

调用线程的start()方法,此线程进入就绪状态。

就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,这些线程也将进入就绪状态。

当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。

锁池里的线程拿到对象锁后,进入就绪状态。

2.2. 运行中状态(RUNNABLE之RUNNING)

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

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

Java线程的生命周期概述版本

3.阻塞状态(BLOCKED)

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

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

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

Java线程的生命周期概述版本

4.等待(WAITING)

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

当线程中调用了没有设置 ​<code>​Timeout​</code>​ 参数的 ​<code>​Object.wait()​</code>​ 方法

当线程调用了没有设置 ​<code>​Timeout​</code>​ 参数的 ​<code>​Thread.join()​</code>​ 方法

当线程调用了 ​<code>​LockSupport.park()​</code>​ 方法

Java线程的生命周期概述版本
关于 ​<code>​LockSupport.park()​</code>​ 方法,这里说一下,我们通过上面知道 ​<code>​Blocked​</code>​ 是针对 ​<code>​synchronized monitor​</code>​ 锁的, 但是在 ​<code>​Java​</code>​ 中实际是有很多其他锁的: 比如 ​<code>​ReentrantLock​</code>​ 等,在这些锁中,如果线程没有获取到锁则会直接进入 ​<code>​Waiting​</code>​ 状态,其实这种本质上它就是执行了 ​<code>​LockSupport.park()​</code>​ 方法进入了​<code>​Waiting​</code>​ 状态

​<code>​**Blocked **​</code>​与 **​<code>​**Waiting**​</code>​ 的区别**

​<code>​Blocked​</code>​ 是在等待其他线程释放 ​<code>​monitor​</code>​ 锁 ,其他条件都满足了吧

​<code>​Waiting​</code>​ 则是在等待某个条件,比如 ​<code>​join​</code>​ 的线程执行完毕,或者是 ​<code>​notify()/notifyAll()​</code>​。

5.超时等待(TIMED_WAITING)

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

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。通过上述图我们可以看到在以下情况会让线程进入 <code>Timed Waiting</code>状态。

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

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

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

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

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

6.终止状态(TERMINATED)

run() 方法执行完毕,线程正常退出。

出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常

二:线程状态间转换

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

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

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

Java线程的生命周期概述版本

只有当执行了​<code>​LockSupport.unpark()​</code>​,或者​<code>​join​</code>​ 的线程运行结束,或者被中断时才可以进入 ​<code>​Runnable​</code>​ 状态。

如下图标注

Java线程的生命周期概述版本

如果通过其他线程调用 ​<code>​notify()​</code>​ 或 ​<code>​notifyAll()​</code>​来唤醒它,则它会直接进入 ​<code>​Blocked​</code>​ 状态,这里大家可能会有疑问,不是应该直接进入 ​<code>​Runnable​</code>​ 吗?这里需要注意一点 ,因为唤醒 ​<code>​Waiting​</code>​ 线程的线程如果调用 ​<code>​notify()​</code>​ 或 ​<code>​notifyAll()​</code>​,要求必须首先持有该 ​<code>​monitor​</code>​ 锁,这也就是我们说的 ​<code>​wait()​</code>​、​<code>​notify​</code>​ 必须在 ​<code>​synchronized​</code>​ 代码块中。

所以处于 ​<code>​Waiting​</code>​ 状态的线程被唤醒时拿不到该锁,就会进入 ​<code>​Blocked​</code>​ 状态,直到执行了 ​<code>​notify()/notifyAll()​</code>​ 的唤醒它的线程执行完毕并释放 ​<code>​monitor​</code>​ 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 ​<code>​Blocked​</code>​ 状态回到 ​<code>​Runnable​</code>​ 状态。

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

Java线程的生命周期概述版本

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

Java线程的生命周期概述版本

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

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

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

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

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

首先这些线程会进入 _EntryList 集合(处于阻塞状态的线程会放到该列表中),当线程A获取到对象的monitor后,Monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex,这时其它线程就无法再获取到该mutex。线程A进入 _Owner区域并把monitor中的owner变量设置为当前线程A,同时monitor中的计数器count加1;

若线程A调用 wait() 方法,释放当前它锁持有的mutex, owner变量恢复为null,count自减1,并且该线程会进入到WaitSet集合(等待集合)中,等待下一次被其他线程调用notify/notifyAll唤醒。;

若线程A执行完毕,也将释放monitor(mutex锁)并复位count的值,以便其他线程获取monitor(mutex锁)进入monitor;

总结一下:

那些处于EntrySet和WaitSet中的线程均处于阻塞状态(OS级别我们叫做阻塞,Java不叫作阻塞 。。​​javascript:void(0)​​ 这边有详解讲解os线程状态跟Java线程状态的区别),

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

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

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

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

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

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

Java线程的生命周期概述版本

总结

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

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

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

所以一个线程只能有一次 <code>New</code>和 <code>Terminated</code>状态,只有处于中间状态才可以相互转换。也就是这两个状态不会参与相互转化