Java|线程的生命周期
基础概念
需要注意区分的是,Java中线程的生命周期概念和操作系统中线程的生命周期概念不是一个东西。
OS
OS中线程的生命周期包含五种:
- 创建态:OS完成线程资源的创建。
- 就绪态:线程启动但还未获得CPU执行权。
- 运行态:线程得到CPU执行权(时间片)正在运行逻辑。
- 阻塞态:线程因某些事件(如IO操作,网络请求)等而阻塞住,此时不会占用CPU执行权。
- 终止态:线程异常/正常执行完毕都会进入终止态。
JVM
JVM中定义的线程的状态包括六种:
- 创建态 NEW: 这一点和OS的创建态是对应的
- 运行态 RUNNABLE: 对应OS中的就绪态+运行态。
- 阻塞态 BLOCKED: 并非OS中的阻塞态,JVM中的阻塞态特指这个线程正在等待获取监视器锁时所处的状态。
- 有限等待 TIMED_WAITING: 等待某个特定条件的发生,通常是其他线程的某个动作,但是是有限期的,超时就不等。
- 无限等待 WAITING: 和 TIMED_WAITING 类似,只不过是无限期的等。
- 终止态 TERMINATED: 线程执行异常或是正常结束后。
下面我们展开来细说。
创建态
当我们新建了一个 Thread 类的对象实例但是还未调用 start() 时,这个线程就一直保持在创建态。
运行态
调用 Thread 类实例的 start() 来进入。
在上面我们提到了 JVM 的创建态 = OS的就绪态 + OS的运行态,这是因为 JVM 将 CPU 的调度权交给操作系统(或者说是 JVM 采用 OS 级别的 CPU 调度),因此,在 JVM 看来,不管线程有没有实际获得 CPU 时间片在跑,只要调用了 thread.start() 以后他就算就绪了,这个线程都是在 RUNNABLE 这个运行态的。
阻塞态
当且仅当尝试进入synchronized锁住的代码块或方法,但其他线程已经持有了这个区域的锁时,这个线程才会进入阻塞态。
BLOCKED 状态是 JVM 专为 synchronized 关键字设计的。
所以 JVM 中的阻塞态并非 OS 中的阻塞态。
无限等待
等待某个特定条件的发生,通常是其他线程的某个动作。
常见进入条件:
Object.wait()注意必须在synchronized中使用LockSupport.park()Thread.join()
常见退出条件:
Object.notify() / notifyAll()注意必须在synchronized中使用LockSupport.unpark()- 被 join 的那个线程执行完了
退出后是进入RUNNABLE状态,此时仍需要申请CPU时间片
有限等待
等待某个特定条件的发生,通常是其他线程的某个动作。
常见触发条件:
Thread.sleep(ms)Object.wait(ms)注意必须在synchronized中使用Thread.join(ms)
常见退出条件:
- 时间到了
Object.notify() / notifyAll()注意必须在synchronized中使用- 被 join 的那个线程执行完了
经典&重点
sleep() 和 wait() 的区别
| 对比项 | Thread.sleep(long millis) | Object.wait() (或 wait(long timeout)) |
|---|---|---|
| 所属类 | Thread类静态方法 | Object类实例方法 |
| 锁的释放 | NO | YES,调用时会释放当前线程持有的对象监视器锁 |
| 使用前提 | NO,任何地方都可以调用 | YES,必须在 synchronized 代码块或方法中使用 |
| 唤醒方式 | 时间到,或被interrupt()中断 | notify/notifyAll/时间到,或被interrupt()中断 |
| 目的/场景 | 手动阻塞当前线程,多用于轮询或让出CPU | 让出资源,多线程写作 |
| 进入状态 | TIMED_WAITING | WAITING/TIMED_WAITING |
JVM的线程状态中,BLOCKED和WAITING的区别
BLOCKED 只会在线程尝试进入synchronized修饰的代码区域时 尝试获取锁失败后进入,是一种被动的动作,尝试获取对象监视锁————失败————进入BLOCKED态等待锁的释放后重新竞争。
而WAITING则是对应线程在某次时间片执行时主动让出CPU执行权,是一种主动操作。
Q:如果一个线程在使用
ReentrantLock.lock()时被阻塞了,它处于什么状态?A:
WAITING状态,因为 ReentrantLock.lock() 底层是基于 LockSupport.park() 实现的,这个方法会使线程进入WAITING状态。
Socket.read() File.read() 后进入什么状态
从主观感受上来说,网络读取IO和文件读取IO,理论上是觉得要进入等待阻塞的,也就是调用操作系统内核态的方法,进入阻塞状态,挂起。在 OS 层面来看是阻塞的。
但是需要注意的是 Socket.read() 和 File.read() 在 JVM 中只是两个本地方法调用。在 JVM 看来,他只是调用了本地方法,线程仍然在运行中,只是没有返回结果。JVM 并不关心操作系统底层在做什么(是在读磁盘还是在等网络包),它只知道该线程正在执行代码(Native 代码),因此在 JVM 的视角线程是 RUNNABLE 状态。
总结
当我们讨论 Java 中线程的生命周期时,需要区别与 OS 层面的线程状态。
共同点在于 NEW 和 TERMINATE 是一样的,但是 JVM 对于 RUNNBALE 和 WAITING 线程状态有着不一样的视角不一样的口径。
如何判定线程状态需要明确边界:
WAITING/TIMED_WAITING:是 JVM 内部显式调用了Object.wait(),Thread.sleep(),LockSupport.park()等方法产生的,JVM 明确知道线程在“等”Java 层面的信号。BLOCKED:是 JVM 内部在处理synchronized锁竞争。而至于网络IO和文件IO等IO操作,这些方法在 Java 中都是本地方法。在底层 OS 的视角来看线程是阻塞状态,未被 CPU 执行。但是在 JVM 的视角来看,未产生任何 Java 层面的线程状态相关信号,自然也未发生线程状态的变动,仍是 RUNNABLE 状态,只是实际上未被 CPU 执行。
之所以 Java 中定义的线程状态会和 OS 层面的线程状态有区别,归根结底在于:
Java 线程状态本质上映射的是 JVM 对线程的管理行为,而不是 CPU 的调度行为。