在本文中,我们将深入探讨 Java 线程的六种状态以及它们之间的转换过程。其实线程状态之间的转换就如同生物生命从诞生、成长到最终死亡的过程一样。也是一个完整的生命周期。
首先我们来看看操作系统中线程的生命周期是如何转换的。
操作系统中的线程状态转换
线程在操作系统中通常有五种状态。
在现代操作系统中,线程被视为轻量级进程,因此操作系统中的线程状态实际上与进程状态类似。
从实际意义上讲,除了 new 和 terminated 状态外,线程主要有以下三种状态:
- 就绪 (Ready) :线程已准备好执行,但可能由于调度策略或其他因素未能获得 CPU ,处于就绪状态的线程只要获得CPU,它就会进入运行状态。
- 正在运行(RUNNING):线程当前正在占用CPU,执行其任务。
- 等待(WAITING):线程正在等待某些事件的发生或资源的获取(例如I/O操作)。
new 和 terminated 状态在线程的实际运行过程中并不频繁涉及,因此讨论这两个状态在实际应用中并不具有太大意义。
Java 线程的 6 种状态
在 Java 中,线程状态的定义与操作系统中的状态并不完全相同。Java 的线程状态提供了更细粒度的管理。
Java 线程状态通过 java.lang.Thread.State 枚举进行定义:
public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}
这些状态之间的转换关系如下图所示:
接下来我们就对Java线程的六种状态进行深入分析。
1. NEW
线程对象被创建出来但是start() 方法还没有被调用,这个时候线程处于new状态。
public class ThreadStateDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {});System.out.println(thread.getState());}
}//输出:
NEW
处于new状态的线程可以扭转到RUNNABLE状态。
2. RUNNABLE
处于new状态的线程,通过调用Thread实例的start()方法可以使线程进入RUNNABLE状态。
注意,Java线程中的RUNNABLE状态对应的是操作系统中线程定义的两种 状态:
- 就绪 (Ready)
- 正在运行(RUNNING)
也就是说在Java中,当一个线程正在运行时,如果CPU时间片用完了, CPU 被调度去执行其他任务,导致该线程暂时停止运行,它的状态仍保持为 RUNNABLE。因为该线程随时可能被重新调度回 CPU 上继续执行。
一个简单的线程示例:
public class ThreadExample {public static void main(String[] args) {// 创建一个线程对象,但尚未启动Thread myThread = new Thread(() -> {System.out.println("线程正在运行...");});// 打印线程状态,是 NEWSystem.out.println("线程状态: " + myThread.getState());// 启动线程myThread.start();// 打印线程状态,是 RUNNABLE(取决于线程调度)System.out.println("线程状态: " + myThread.getState());}
}
前面讲了处于NEW状态的线程,通过调用Thread实例的start()方法进入RUNNABLE状态。关于start()方法,其实有两个值得思考的问题:
- 是否可以在同一个线程对象上重复调用 start() 方法?
- 如果一个线程已经执行完毕,处于 TERMINATED 状态,是否可以再次调用 start() 方法?
要回答这两个问题,我们可以查看 start() 方法的源码。
public synchronized void start() {if (threadStatus != 0)throw new 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() 方法时,就会直接抛出 IllegalThreadStateException 异常。
接着,start() 方法调用了一个名为 start0() 的方法,该方法是一个本地方法(native method),由底层操作系统或虚拟机实现,因此我们无法从 Java 代码中看到它对 threadStatus 的具体处理方式。不过,这并不妨碍我们了解其行为。
我们可以通过在调用 start() 方法时打印出当前线程的状态,然后尝试多次调用 start() 方法,以观察并理解 IllegalThreadStateException 异常的触发条件和线程状态的变化。
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.IllegalThreadStateExceptionat java.lang.Thread.start(Thread.java:708)at thread.basic.ThreadStateDemo.main(ThreadStateDemo.java:11)
可以看到,第一次调用 start() 方法是没有问题的,但在第二次调用时会报错。错误信息显示在 java.lang.Thread.start(Thread.java:708) 处,这个错误的原因是线程的状态检查失败。
这是因为当线程的 start() 方法第一次被调用时,线程被正确启动并进入 RUNNABLE 状态。第二次再调用start()时,由于线程的状态不再是初始NEW状态(0),直接抛出异常。
最后总结一下:
- 如果尝试在同一个线程上重复调用 start() 方法,会抛出 IllegalThreadStateException 异常,也就是说同一个线程对象只能启动一次。
- 如果线程已经完成(处于 TERMINATED 状态),再次调用 start() 方法也是不允许的,会同样抛出 IllegalThreadStateException 异常。线程一旦结束就不能再被重新启动。
处于RUNNABLE状态的线程根据不同的条件可以扭转到BLOCKED,WAITING,TIMED_WAITING,TERMINATED等状态。
3. BLOCKED
当线程处于 BLOCKED 状态时,表示它正在等待获取一个锁,以便进入同步区域。
我们可以用一个生活中的例子来说明 BLOCKED 状态:假设你去银行办理业务,当你走到某个窗口时,发现已经有一个人在你前面,此时你必须等到前面的人办完业务并离开窗口后,才能开始办理你的业务。
在这个例子中,你就是线程 B,前面的人是线程 A。当 A 正在占用窗口(即锁),而 B 需要等待 A 完成并释放窗口资源,这期间线程 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() 方法来模拟业务处理所需的时间。
你可能会觉得线程 A 会首先调用同步方法,而在同步方法内调用 Thread.sleep() 方法,使其进入 TIMED_WAITING 状态;与此同时,线程 B 则在等待线程 A 释放锁,因此它的状态会是 BLOCKED。然而,实际情况并不总是如此!这是因为:
- 除了线程 A 和线程 B,程序中还有一个主线程在运行。
- 当我们调用 start() 方法启动线程时,线程从调用 start() 到真正开始执行 run() 方法之间存在一定的时间差。在这个时间差内,CPU 的调度竞争结果会导致不同的输出。
下面是一种可能的输出:
//输出:
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[B] performs business processing
这种场景下,线程 A 正在执行 businessProcessing 方法,线程 B 正在等待获取 businessProcessing 方法的锁,因此它处于 BLOCKED 状态。
如果你希望线程 A 打印出 TIMED_WAITING 状态,而线程 B 打印出 BLOCKED 状态,可以稍微修改主线程的逻辑。在调用 A.start() 后,让主线程“休息一会儿”,使用 Thread.sleep() 方法让线程 A 有时间去获取锁。
注意的是,主线程的休眠时间应该足够长,确保线程 A 正在执行并进入 TIMED_WAITING 状态,但又不应太长,以免线程 A 完成任务并释放锁。这样,在线程 A 执行期间,线程 B 仍然会尝试获取锁并进入 BLOCKED 状态。
这样一来,我们可以有效地控制两个线程的状态,使得线程 A 进入 TIMED_WAITING 状态,而线程 B 则处于 BLOCKED 状态。
public static void main(String[] args) throws InterruptedException {BlockCase blockCase = new BlockCase();Thread A = new Thread(blockCase::businessProcessing, "A");Thread B = new Thread(blockCase::businessProcessing, "B");// Fixed output TIMED_WAITING state and BLOCKED stateA.start();Thread.sleep(1000); //Sleep time should be less than business processing timeB.start();System.out.println("Thread[" + A.getName() + "] state:" + A.getState());Sy stem.out.println("Thread[" + B.getName() + "] state:" + B.getState());}//输出:
Thread[A] performs business processing
Thread[A] state:TIMED_WAITING
Thread[B] state:BLOCKED
Thread[B] performs business processing
在这个例子中,两个线程的状态会按照以下步骤转换:
线程 A 的状态转换过程:
- NEW: 线程 A 被创建,但还未启动。
- RUNNABLE: 调用 A.start() 后,线程 A 进入可运行状态,等待被 CPU 调度。
- TIMED_WAITING: 线程 A 获取到锁后,调用 Thread.sleep() 方法进入计时等待状态。
- RUNNABLE: 等待时间结束,线程 A 重新进入可运行状态。
- TERMINATED: 线程 A 执行完任务,进入终止状态。
线程 B 的状态转换过程:
- NEW: 线程 B 被创建,但还未启动。
- RUNNABLE: 调用 B.start() 后,线程 B 进入可运行状态,等待被 CPU 调度。
- BLOCKED: 线程 B 尝试获取锁失败,因为线程 A 已经持有锁,所以 B 进入阻塞状态。
- RUNNABLE: 线程 A 释放锁后,线程 B 获取到锁,进入可运行状态。
- TIMED_WAITING: 线程 B 进入临时等待状态(例如,通过 Thread.sleep())。
- RUNNABLE: 等待时间结束,线程 B 重新进入可运行状态。
- TERMINATED: 线程 B 执行完任务,进入终止状态。
处于BLOCKED状态的线程获取到锁后可以扭转到RUNNABLE状态。
4. WAITING
线程进入WAITING状态的方式有三种:
- Object.wait():将当前线程置于等待状态,直到另一个线程调用同一对象的 notify() 或 notifyAll() 方法来唤醒它。
- Thread.join():使当前线程等待指定的线程执行完毕后再继续运行。底层实现是调用 Object.wait() 方法。
- LockSupport.park():使当前线程进入等待状态,直到被显式地唤醒。它的控制权完全取决于是否获得了唤醒权限。
让我们继续用之前的银行办理业务的例子来解释 WAITING 状态。
假设你在银行办理业务时,终于轮到你到柜台办理了。但是,不幸的是,柜台的电脑突然坏了。为了完成业务,你必须等待维修人员修好电脑后才能继续办理。
在这个场景中,假设你是线程 A,维修人员是线程 B。尽管你已经在柜台前等待(即获得了锁),但是你还要释放锁,此时线程A的状态是WAITING,然后线程B获得锁,进入RUNNABLE状态。
如果线程 B 不主动唤醒线程 A(通过调用 notify() 或 notifyAll() 方法),线程 A 将会一直处于等待状态,无法继续执行。
以下是一个简单的代码示例,演示了如何使用 Object.wait() 和 notify() 方法来实现这种行为:
public class WaitingCase {private synchronized void businessProcessing() {try {System.out.println("Thread[" + Thread.currentThread().getName() + "] expects to process business, but the computer is broken");// Release the monitor(lock)wait();// business processingSystem.out.println("Thread[" + Thread.currentThread().getName() + "] continues to process business");Thread.sleep(2000L);} catch (InterruptedException e) {e.printStackTrace();}}private synchronized void repairComputer() {System.out.println("Thread[" + Thread.currentThread().getName() + "] comes to repair the computer");try {// Simulated RepairThread.sleep(1000L);System.out.println("Thread[" + Thread.currentThread().getName() + "] has completed the repair.");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); //Used to ensure that thread A grabs the lock first. Sleep time should be less than repair timeB.start();System.out.println("Thread[" + A.getName() + "] state:" + A.getState());System.out.println("Thread[" + B.getName() + "] state:" + B.getState());}
}//输出:
Thread[A] expects to process business, but the computer is broken
Thread[B] comes to repair the computer
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] has completed the repair.
Thread[A] continues to process business
关于 wait() 方法,有几个关键点需要特别强调:
-
持有锁:在调用 wait() 方法之前,线程必须先获得对象的监视器(锁)。换句话说,调用 wait() 的线程必须在同步代码块或同步方法中运行,即持有对象的锁。
-
释放锁:当线程调用 wait() 方法时,它会释放当前持有的锁,并进入等待状态。线程将保持在等待状态,直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。
-
notify() 方法:调用 notify() 方法只能唤醒一个正在等待该锁的线程。如果有多个线程在等待同一个对象的锁,notify() 方法只会唤醒其中一个线程,这个线程并不是固定的,具体哪个线程被唤醒取决于线程调度的具体实现。
-
notifyAll() 方法:调用 notifyAll() 方法会唤醒所有正在等待该锁的线程。这些被唤醒的线程会竞争重新获得锁,但并不保证它们会立即得到 CPU 时间片,具体的调度顺序取决于操作系统的线程调度策略。
我们再来看看Thread.join()方法。
join() 方法用于使调用线程暂停执行,直到被调用的线程执行完毕。调用 join() 的线程将进入 WAITING 状态,直到目标线程完成执行。这个方法常用于主线程中,确保在继续执行之前等待其他线程完成。
我们来回顾一下之前的 BlockCase 示例,其中 A.start() 和 B.start() 都是在主线程中直接调用的。这就像是让多个线程竞争窗口的使用权。如果参与竞争的线程越来越多,窗口就会变得非常拥挤。
为了改善这个问题,银行引入了一个新的办法:给每个办理业务的客户一个编号,按编号叫号。只有被叫到的客户才能到窗口办理业务,其余的客户则可以在休息区等待。
现在,我们可以在之前的 BlockCase 示例中扩展这个想法,假设我们有三个线程来模拟这个场景。每个线程代表一个客户,我们将使用 join() 方法来确保主线程等待所有客户(线程)完成业务后才继续执行。
public class JoinCase {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) 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("Please ask thread A to go to the window to handle the business.");A.start();A.join();System.out.println("Please ask thread B to go to the window to handle the business.");B.start();B.join();System.out.println("Please ask thread C to go to the window to handle the business.");C.start();}
}//输出:
Please ask thread A to go to the window to handle the business.
Thread[A] performs business processing
Please ask thread B to go to the window to handle the business.
Thread[B] performs business processing
Please ask thread C to go to the window to handle the business.
Thread[C] performs business processing
您可以尝试多次执行该程序,并且总是会得到相同的结果。
处于WAITING状态的线程被其他线程唤醒可以扭转到RUNNABLE状态。
5.TIMED_WAITING
超时等待状态 (TIMED_WAITING) 是线程在指定时间内等待的状态,时间到后线程会自动唤醒。以下方法可以使线程进入超时等待状态:
- 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):与 parkNanos() 类似,但使用绝对时间戳作为参数。
处于TIMED_WAITING状态的线程被其他线程唤醒或等待的时间到了以后被扭转到RUNNABLE状态。
6. TERMINATED
当线程已完成执行时,处于TERMINATED状态。
已经终止的线程无法再扭转到其它状态