线程状态的实质
线程状态实质上是一个变量的值而已
Thread
类中的一个变量 threadStatus
为方便理解,Thread 中还有一个内部枚举类 State
1 | /** |
(看这么长的注释就可见其不简单)
NEW
一切的起点,要从把一个 Thread 类的对象创建出来,开始说起
1 | Thread t = new Thread(); |
也可以使用含参构造
1 | Thread t = new Thread(runnable, threadName); |
还可以 new 一个继承了 Thread 的子类
1 | Thread t = new SubThread(); |
线程池中的线程也是 new 出来的
1
2
3
4
5
6
7
8
9 public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(...);
...
return t;
}
}
}
一切的一切就到了 Thread 的构造函数中,而 Thread 的构造函数又是调用的 Thread 中的 init
方法
1 | private void init(ThreadGroup g, Runnable target, String name, |
这个 init 方法,仅仅是给该 Thread 类的对象中的属性,附上值,除此之外啥也没干
它没有给 theadStatus 再次赋值,所以它的值仍然是其默认值——STATE.NEW
RUNNABLE
在调用 Thread 的 start 方法后才算是启动了
1 | public synchronized void start() { |
start 方法一开始就会对线程的状态进行判断,如果线程的状态不是 NEW,就会抛出异常
其核心的启动方法为 start0() 这是一个本地方法
1 | private native void start0(); |
进入 jvm 源码,可以查到这个方法
hotspot/src/os/linux/vm/os_linux.cpp
1 | pthread_create(...); |
unix 创建线程的方法,pthread_create
此时,在操作系统内核中,才有了一个真正的线程,被创建出来
而 linux 操作系统,是没有所谓的刚创建但没启动的线程这种说法的,创建即开始运行
虽然无法从源码发现线程状态的变化,但通过 debug 的方式,我们看到调用了 Thread.start() 方法后,线程的状态变成了 RUNNABLE,运行态
通过这部分,我们知道如下几点:
- 在 Java 调用 start() 后,操作系统中才真正出现了一个线程,并且立刻运行
- Java 线程与操作系统内核中的线程是一对一的关系
- 调用 start 后,线程状态变为 RUNNABLE,这是由 native 方法里的某部分代码造成的
RUNNING 和 READY
CPU 一个核心,同一时刻,只能运行一个线程
具体执行哪个线程,要看操作系统的调度机制
所以,上面的 RUNNABLE 状态,准确说是,可以随时运行的就绪状态
处于这个状态中的线程,又可以细分为两种状态
- 正在 CPU 中运行的 RUNNING 状态
- 等待 CPU 分配时间片来运行的 READY 状态
这里的 RUNNING 和 READY 状态,是为了方便理解而创造出来的
无论是 Java 语言,还是操作系统,都不区分这两种状态,在 Java 中统统叫 RUNNABLE
TERMINATED
当一个线程执行完毕(或者因异常中断,或调用已经不建议的 stop 方法),线程的状态就变为 TERMINATED
BLOCKED
上面是最常见,最简单的线程生命周期,NEW -> RUNNABLE -> TERMINATE
1 | private static final Object lock = new Object(); |
题外话:上面代码要特别注意给全局变量 i 声明
volatile
输出:
1 | t1 state: RUNNABLE |
由此得出以下转换关系
需要注意的是,从 BLOCKED 到 RUNNABLE 后,具体是 RUNNING 还是 READY 这个并不是由 java 决定,取决于 CPU 的调度
如果不考虑虚拟机对 synchronized 的极致优化
当进入 synchronized 块或方法,获取不到锁时,线程会进入一个 该锁对象的同步队列
当持有锁的这个线程,释放了锁之后,会唤醒该锁对象同步队列中的所有线程,这些线程会继续尝试抢锁。如此往复
比如,现在有一个对象锁 🔒A,有 ①,②,③,④ 个线程去抢这个锁
抢到锁的则进入 RUNNABLE 状态,否则进入同步队列为 BLOCKED 状态
线程 ① 抢到锁,线程 ②,③,④ 进入 🔒A 的同步队列
线程 ① 释放锁,线程 ②,③,④ 重新变为 RUNNABLE,继续抢锁,此时线程 ③ 抢到了锁,线程 ②,④ 再次进入 🔒A 的同步队列
WAITING
这部分是最复杂的,同时也是面试中考点最多的,将分成三部分讲解。这三部分有很多相同但地方,并不是孤立的知识点
wait/notify/notifyAll
1 | new Thread(() -> { |
调用 lock.wait() 方法,会发生三件事:
- 释放 🔒
- 线程变为 WAITING 状态
- 线程进入 🔒 对象的 等待队列
如果想要将线程 ① 从 🔒 对象的等待队列中取出,需要另一个线程进行唤醒
1 | new Thread(() -> { |
这时线程 ① 的状态变为 RUNNABLE,需要注意的是,线程 ① 仍然要抢锁,如果抢锁失败,就和上面 BLOCKED 流程一样了
notify 与 notifyAll 的区别在于,notify 唤醒 🔒 对象的等待队列中 任意一个 线程,而 notifyAll 是清空等待队列,唤醒所有的线程,它们都会重新去抢锁
现在,整个流程图变成这样
join
1 | public static void main(String[] args) { |
out:
1 | t1 state:RUNNABLE |
join 也能使线程进入 WAITING 状态
状态更新
join 是如何将线程放入等待队列的呢?
查看 Thread.join()
方法可以看到底层逻辑
1 | public final synchronized void join(long millis) throws InterruptedException { |
join 的底层就是 wait
与之前自己调用 wait 不同的是
- join 中调用的 wait 是线程对象的,而前面自己调用的是锁对象的
- join 调用的 wait 不需要用户执行 notify/notifyAll,而前面自己调用是需要另一个线程调用 notify/notifyAll
那 join 中调用 wait 后,是如何唤醒的呢?
查看 JVM 源码 hotspot/src/share/vm/runtime/thread.cpp
1 | void JavaThread::exit(...) { |
所以还是通过 notifyAll 来唤醒的,只不过是 jvm 等线程执行结束后来唤醒的
所以 join 可以说是等同于 wait,这样状态的变化更清晰了
既然 Thread.join 会对线程对象进行上锁,那么如果有两个线程 t1,t2,t1 中调用
t2.join()
,t2 中调用t1.join()
,如此一来,是否就导致死锁?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 static Thread[] t = new Thread[2];
public static void main(String[] args) {
t[0] = new Thread(() -> {
try {
t[1].join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t[1] = new Thread(() -> {
try {
t[0].join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t[0].start();
t[1].start();
}
park/unpark
park 与 unpark 是 java.util.concurrent.locks.LockSupport
下的两个方法
LockSupport 是 JUC 包中的一个实用工具类,提供基本的线程阻塞(parking)和解锁(unparking)机制。它通常用于实现更高级别的同步实用程序,例如 semaphore, mutexes 和 condition variables
用法一看便知
1 | public static void main(String[] args) { |
Out:
1 | t1: Parking... |
park 可以使线程进入 WAITING 状态
unpark 可以使线程回到 RUNNABLE 状态
不过,与 wait/notify 不同的是
- park/unpark 与锁无关
- unpark 可以精准唤醒指定线程
- park/unpark 无顺序要求,可以先 unpark
整个状态就又发生更新
TIMED_WAITING
这部分就再简单不过了,将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数,就变成了将线程变成 TIMED_WAITING 状态的方法了
需要注意的是,从 TIMED_WAITING 到 RUNNABLE 不仅可以通过主动调用方法来改变,还能 通过超时时间来被动实现
与此同时,还有一个常见的方法也能使线程从 RUNNABLE 到 TIMED_WAITING
就是 Thread.sleep(long)
如此一来,整个线程的状态转换就很清晰明了了
Yield
yield 即 「谦让」,是 Thread 中的方法
yield 用于让出当前线程的时间片,给其他线程来执行,但只是对线程调度器的 建议,是否切换完全取决于 CPU
yield 很卑微,并不能改变线程的执行状态,不如 wait 可以释放锁,不如 sleep 可以暂停线程,一句话总结就是没啥鸟用
ReentrantLock
ReentrantLock 是除 Synchronized 外,最常用的锁,但它们的底层实现并不相同,ReentrantLock 是大佬「Doug Lea」基于 AQS 实现的,而 Synchronized 是基于 JVM 实现的,ReentrantLock 有 Synchronized 之外的更「高级」的功能
这里只分析 ReentrantLock 对线程状态的影响
1 | private static final ReentrantLock LOCK = new ReentrantLock(); |
Out:
1 | t1 running... |
当 t1 获取到锁,而 t2 没获取到锁在等待时,t2 的状态并 不是 BLOCKED 而是 WAITING
这与 Synchronized 有很大的不同
原因在于 ReentrantLock 是基于 AQS 的,而 AQS 的底层,是用 park 和 unpark 来挂起和唤醒线程,所以状态变为 WAITING
await/signal/signalAll
ReentrantLock(AQS) 还提供了 Condition 类来实现线程间的协调,可以通过 Condition 中的 await 方法实现线程等待,signal 或 signalAll 方法实现线程的唤醒,十分类似 wait/notify/notifyAll
1 | private static final ReentrantLock lock = new ReentrantLock(); |
Out:
1 | t1 running... |