在本文中,我们将深入探讨 Java 线程的六种状态以及它们之间如何相互转换。线程状态的转换就如同生物从出生、成长到最终死亡的过程,也有一个完整的生命周期。
操作系统中的线程状态
首先,让我们看看操作系统中线程的生命周期是如何流转的。
在操作系统中,线程共有 5 种状态:
- 新建(NEW):线程已创建,但尚未开始执行。
- 就绪(READY):线程等待使用 CPU,在被调度程序调用后可进入运行状态。
- 运行(RUNNING):线程正在使用 CPU。
- 等待(WAITING):线程因等待事件或其他资源(如 I/O)而被阻塞。
- 终止(TERMINATED):线程已完成执行。
Java 线程的 6 种状态
Java 中线程状态的定义与操作系统中的并不完全相同,查看 JDK 中的java.lang.Thread.State
可以找到 Java 线程状态的定义:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
它们之间的流程关系如下图所示:
接下来,我们将对 Java 线程的六种状态进行深入分析。
NEW(新建)
处于NEW
状态的线程实际上还没有启动。也就是说,Thread 实例的start()
方法还没有被调用。可流转状态:RUNNABLE
public class ThreadStateDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
}
}
输出:
NEW
RUNNABLE(可运行)
Java 中的Runable
状态对应操作系统线程状态中的两种状态,分别是Running
和Ready
,也就是说,Java 中处于Runnable
状态的线程有可能正在执行,也有可能没有正在执行比如正在等待被分配 CPU 资源。
所以,如果一个正在运行的线程是Runnable
状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是Runnable
,因为它有可能随时被调度回来继续执行任务。可流转状态:BLOCKED
、WAITING
、TIMED_WAITING
、TERMINATED
在 Java 中,线程通过调用Thread
实例的start()
方法进入RUNNABLE
状态。
关于start()
方法,有两个问题需要思考一下:
- 能否对同一个线程重复调用
start()
方法? - 如果一个线程已经执行完毕并处于
TERMINATED
状态,是否可以再次调用该线程的start()
方法?
为了分析这两个问题,我们先来看看start()
方法的源码:
public synchronized void start() {
if (threadStatus!= 0)
thrownew IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
我们可以看到,在start()
方法内部,有一个threadStatus
变量。如果它不等于 0,调用start()
方法将直接抛出异常。
接下来,调用了一个start0()
方法,但它是一个本地方法,无法知道方法内如何处理threadStatus
。但没关系,我们可以在调用start()
方法后输出当前状态,并尝试再次调用start()
方法:
public class ThreadStateDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
thread.start(); // 第一次调用
System.out.println(thread.getState());
thread.start(); // 第二次调用
}
}
输出:
NEW
RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at thread.basic.ThreadStateDemo.main(ThreadStateDemo.java:11)
可以看到,第一次调用start()
方法是可以的,但第二次调用会报错,java.lang.Thread.start(Thread.java:708)
指的是状态检查失败:
查看获取当前线程状态的源码:
public State getState() {
// 获取当前线程状态
return sun.misc.VM.toThreadState(threadStatus);
}
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} elseif ((var0 & 1024) != 0) {
return State.BLOCKED;
} elseif ((var0 & 16) != 0) {
return State.WAITING;
} elseif ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} elseif ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
我们可以看到,只有State.NEW
的状态值被计算为 0。
因此,结合上面的源码,我们可以得到两个问题的答案都是不可行的。start()
方法只能在NEW
状态下调用。
BLOCKED(阻塞)
处于BLOCKED
状态的线程正在等待锁的释放。可流转状态:RUNNABLE
我们用一个生活中的例子来说明BLOCKED
状态:
假设你去银行办理业务。当你来到某个窗口时,发现前面已经有人了。这时,你必须等待前面的人离开窗口,才能办理业务。
假设你是线程 B,前面的人是线程 A。此时,A 占有了锁(银行办理业务的窗口),B 正在等待锁的释放,线程 B 此时就处于 BLOCKED 状态。
代码示例如下:
public class BlockCase {
private synchronized void businessProcessing() {
try {
System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BlockCase blockCase = new BlockCase();
Thread A = new Thread(blockCase::businessProcessing, "A");
Thread B = new Thread(blockCase::businessProcessing, "B");
A.start();
B.start();
System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
}
}
这里使用Thread.sleep()
来模拟业务处理所需的时间。
输出:
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[B] performs business processing
注意:如果多次执行输出结果可能不相同,这是因为两个线程谁先被调度是随机的
WAITING(等待)
等待状态。处于等待状态的线程需要其他线程唤醒才能转换为RUNNABLE
状态。可流转状态:RUNNABLE
调用以下三种方法会使线程进入等待状态:
Object.wait()
:使当前线程进入等待状态,直到另一个线程唤醒它;Thread.join()
:等待指定的线程执行完毕。底层调用的是Object
实例的wait
方法;LockSupport.park()
:在获得调用权限之前禁止当前线程进行线程调度。
我们主要解释Object.wait()
和Thread.join()
的用法。
继续前面的例子来解释 WAITING 状态:
你在银行等了很久,终于轮到你来办理业务了。但不幸的是,你到达柜台后,柜台的电脑突然坏了。你必须等待维修人员修好电脑后才能继续办理业务。
此时,假设你是线程 A,维修人员是线程 B。虽然你已经拥有了锁(窗口),但你仍然需要释放锁。此时,线程 A 的状态是 WAITING,然后线程 B 获得锁并进入 RUNNABLE 状态。
如果线程 B 没有主动唤醒线程 A(通过
notify()
或notifyAll()
),线程 A 只能一直等待。
Object.wait()
对于这个例子,我们使用wait()
、notify()
实现,如下所示:
public class WaitingCase {
private synchronized void businessProcessing() {
try {
System.out.println("Thread[" + Thread.currentThread().getName() + "] 处理业务,但电脑坏了。");
// 释放窗口资源(锁)
wait();
// 业务处理
System.out.println("Thread[" + Thread.currentThread().getName() + "] 继续处理业务。");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void repairComputer() {
System.out.println("Thread[" + Thread.currentThread().getName() + "] 维修电脑。");
try {
// 模拟维修
Thread.sleep(1000);
System.out.println("Thread[" + Thread.currentThread().getName() + "] 电脑维修好了。");
notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
WaitingCase blockedCase = new WaitingCase();
Thread A = new Thread(blockedCase::businessProcessing, "A");
Thread B = new Thread(blockedCase::repairComputer, "B");
A.start();
Thread.sleep(500); // 用于确保线程 A 先抢到锁。睡眠时间应该小于维修时间
B.start();
System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
}
}
输出:
Thread[A] 处理业务,但电脑坏了。
Thread[B] 维修电脑。
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] 电脑维修好了。
Thread[A] 继续处理业务。
关于wait()
方法,这里有一些需要注意的点:
- 线程在调用
wait()
方法之前必须持有对象的锁。 - 当线程调用
wait()
方法时,它会释放当前的锁,直到另一个线程调用notify()
或notifyAll()
方法唤醒等待锁的线程。 - 调用
notify()
方法只会唤醒一个等待锁的线程。如果有多个线程在等待锁,之前调用wait()
方法的线程可能不会被唤醒。 - 调用
notifyAll()
方法后,所有等待锁的线程都会被唤醒,但时间片可能不会立即分配给刚刚放弃锁的线程,这取决于系统的调度。
Thread.join()
join()
方法暂停调用线程的执行,直到被调用的对象完成执行。此时,当前线程处于WAITING
状态。
join()
方法通常在主线程中使用,以等待其他线程完成后主线程再继续执行。
现在来银行办理业务的人越来越多了,如果每次窗口空闲出来后所有人都会争抢窗口的话,会造成资源的浪费。
银行想到了一个办法。每个来办理业务的客户都会得到一个序列号,窗口会依次叫号。只有被叫到的客户才需要去窗口,否则他们可以留在休息区。
让我们扩展前面BlockCase
中的例子来简单实现这样的功能:
public class JoinCase {
private synchronized void businessProcessing() {
try {
System.out.println("Thread[" + Thread.currentThread().getName() + "] 办理业务。");
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
JoinCase blockedCase = new JoinCase();
Thread A = new Thread(blockedCase::businessProcessing, "A");
Thread B = new Thread(blockedCase::businessProcessing, "B");
Thread C = new Thread(blockedCase::businessProcessing, "C");
System.out.println("请让线程 A 到窗口处理业务。");
A.start();
A.join();
System.out.println("请让线程 B 到窗口处理业务。");
B.start();
B.join();
System.out.println("请让线程 C 到窗口处理业务。");
C.start();
}
}
输出:
请让线程 A 到窗口处理业务。
Thread[A] 办理业务。
请让线程 B 到窗口处理业务。
Thread[B] 办理业务。
请让线程 C 到窗口处理业务。
Thread[C] 办理业务。
你可以多次尝试执行这个程序,每次都会得到相同的结果。
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)
:与上述类似,也禁止线程调度指定的时间。
我们继续上面的例子来解释 TIMED_WAITING 状态:
当你轮到你办理业务员时,之前办理业务的客户说他忘记处理一个业务,现在需要处理,要求你给他 5 分钟时间。你同意了然后就去休息区休息,当 5 分钟过去后,你重新去办理业务。
此时,你仍然是线程 A,插队的朋友是线程 B。线程 B 让线程 A 等待指定的时间,在这段等待期间,A 处于 TIMED_WAITING 状态。
等待 5 分钟后,A 自动唤醒,获得了竞争锁(窗口)的资格。
可以使用Object.wait(long timeout)
方法实现。Object.wait(long timeout)
方法与无参数的wait()
方法功能相同,都可以被其他线程调用notify()
或notifyAll()
方法唤醒。
public class TimedWaitingCase {
privatestaticfinal Object lock = new Object();
public static void main(String[] args) {
// 线程 A:模拟等待超时
Thread threadA = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("线程 A 开始等待,最多等待 5 秒...");
// 线程 A 进入 TIMED_WAITING 状态,等待 5 秒
lock.wait(5000);
System.out.println("线程 A 等待结束,继续执行。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 线程 B:模拟在等待期间唤醒线程 A
Thread threadB = new Thread(() -> {
synchronized (lock) {
try {
// 线程 B 先睡眠 2 秒,模拟一些处理时间
Thread.sleep(2000);
System.out.println("线程 B 尝试唤醒等待的线程 A...");
// 唤醒等待的线程 A
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程 A
threadA.start();
// 启动线程 B
threadB.start();
}
}
不同之处在于,带参数的wait(long)
方法即使没有其他线程唤醒它,也会在指定时间后自动唤醒,使其获得竞争锁的资格。
TERMINATED(终止)
再来看看最后一种状态,Terminated
终止状态,要想进入这个状态有两种可能。
run()
方法执行完毕,线程正常退出。- 出现一个没有捕获的异常,终止了
run()
方法,最终导致意外终止。
可流转状态:无
总结
Java 线程的六种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)描述了线程从创建到终止的完整生命周期。理解这些状态及其转换机制,有助于更好地掌握多线程编程,避免常见的并发问题。Java 线程状态与操作系统线程状态虽有相似之处,但 Java 对其进行了更细粒度的划分,以适应复杂的并发场景。掌握这些状态及其转换,是编写高效、稳定多线程程序的关键。