Java 线程的 6 种状态及转化

前言

线程是 JVM 执行任务的最小单元,理解线程的状态转换是理解后续多线程问题的基础。

当我们说一个线程的状态时,其实说的就是一个变量的值,在 Thread 类中的一个变量,叫 private volatile int threadStatus = 0;

这个值是个整数,不方便理解,可以通过映射关系(VM.toThreadState),转换成一个枚举类。

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}

java.lang.Thread.State 枚举类中定义了 6 种线程的状态,可以调用线程 Thread 中的 getState() 方法获取当前线程的状态。

在这里插入图片描述
Java线程状态转换图
在这里插入图片描述

NEW (新建状态)

当创建一个线程后,还没有调用 start() 方法时,此时这个线程的状态,是 NEW(初始态)

Thread t = new Thread();

在这里插入图片描述

RUNNABLE(运行状态)

当 Thread 调用 start 方法后,线程进入 RUNNABLE 可运行状态
在这里插入图片描述

在 RUNNABLE 状态当中又包括了 RUNNING 和 READY 两种状态。

RUNNING(运行中) 和 READY(就绪)

就绪状态(READY)

当线程对象调用了 start() 方法之后,线程处于就绪状态,会存储在一个就绪队列中,就绪意味着该线程可以执行,但具体啥时候执行将取决于 JVM 里线程调度器的调度。

It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.

这里的意思是:

  • 不允许对一个线程多次使用 start
  • 线程执行完成之后,不能试图用 start 将其唤醒

那么在什么情况下会变成就绪状态呢,如下情况:

  • 线程调用 start(),新建状态转化为就绪状态
  • 线程 sleep(long) 时间到,等待状态转化为就绪状态
  • 阻塞式 IO 操作结果返回,线程变为就绪状态
  • 其他线程调用 join() 方法,结束之后转化为就绪状态
  • 线程对象拿到对象锁之后,也会进入就绪状态

运行状态(RUNNING)

处于就绪状态的线程获得了 CPU 之后,真正开始执行 run() 方法的线程执行体时,意味着该线程就已经处于运行状态。需要注意的是,对于单处理器,一个时刻只能有一个线程处于运行状态,对于抢占式策略的系统来说,系统会给每个线程一小段时间处理各自的任务。时间用完之后,系统负责夺回线程占用的资源。下一段时间里,系统会根据一定规则,再次进行调度。

那么在什么时候运行状态变为就绪状态的呢?

  • 线程失去处理器资源。线程不一定完整执行的,执行到一半,说不定就被别的线程抢走了
  • 调用 yield() 静态方法,暂时暂停当前线程,让系统的线程调度器重新调度一次,它自己完全有可能再次运行

在这里插入图片描述

TERMINATED(终止状态)

当一个线程执行完毕,线程的状态就变为 TERMINATED。

TERMINATED 终止状态,以下两种情况会进入这个状态:

  • 当线程的 run() 方法完成时,或者主线程的 main() 方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生
  • 在一个终止的线程上调用 start() 方法,会抛出 java.lang.IllegalThreadStateException 异常

为什么会报错呢,因为 start 方法的已经定义好了:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    ...
}

在这里插入图片描述

NEW、RUNNABLE、TERMINATED 案例

下面用代码实现看看:

	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread();
		System.out.println("创建线程后,线程的状态为:"+ thread .getState());
		myThread.start();
		System.out.println("调用start()方法后线程的状态为:"+thread .getState());
		//休眠30毫秒,等待 Thread 线程执行完
		Thread.sleep(30);
		System.out.println(Thread.currentThread().getName()+"线程运行");
		System.out.println("执行完线程的状态为:"+ thread .getState());
	}
创建线程后,线程的状态为:NEW
调用start()方法后线程的状态为:RUNNABLE
main线程运行
执行完线程的状态为:TERMINATED

从以上代码实现可知:

  • 刚创建完线程后,状态为 NEW
  • 调用了 start() 方法后线程的状态变为 RUNNABLE
  • 然后,我们看到了 run() 方法的执行,这个执行,是在主线程 main 中调用 start() 方法后线程的状态为 RUNNABLE 输出后执行的
  • 随后,我们让 main 线程休眠了30毫秒,等待 Thread 线程退出
  • 最后再打印 Thread 线程的状态,为 TERMINATED

BLOCKED (阻塞状态)

在 RUNNABLE状态 的线程进入 synchronized 同步块或者同步方法时,如果获取锁失败,则会进入到 BLOCKED 状态。当获取到锁后,会从 BLOCKED 状态恢复到 RUNNABLE 状态。

因此,我们可以得出如下转换关系:

在这里插入图片描述

下面用代码实现看看:

	public static synchronized void method01() {
		System.out.println(Thread.currentThread().getName()+"开始执行主线程的方法");
		try {
			Thread.sleep(10);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"主线程的方法执行完毕");
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread threadA = new Thread(() -> method01(), "A-Thread");
		Thread threadB = new Thread(() -> method01(), "B-Thread");

		threadA.start();
		threadB.start();

		System.out.println("线程 A 的状态为:"+ threadA.getState());
		System.out.println("线程 B 的状态为:"+ threadB.getState());

	}

A-Thread开始执行主线程的方法
线程A的状态为:RUNNABLE
线程B的状态为:BLOCKED
A-Thread主线程的方法执行完毕
B-Thread开始执行主线程的方法
B-Thread主线程的方法执行完毕

从上面代码执行可知,A 线程优先获得到了锁,状态为 RUNNABLE,这时 B 线程处于 BLOCKED 状态,当 A 线程执行完毕后,B 线程接着执行对应方法。

WAITING(等待状态)

这部分是比较复杂的,同时也是面试中问得最多的,处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。线程进入 Waiting 状态和回到 Runnable 有三种可能性:

wait/notify

没有设置 Timeout 参数的 Object.wait() 方法,如果其他线程调用 notify() 或 notifyAll() 方法来唤醒它,它会直接进入 Blocked 状态,因为唤醒 WAITING 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 WAITING 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 方法唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。

这里的 notify 是只唤醒一个线程,而 notifyAll 是唤醒所有等待队列中的线程。

在这里插入图片描述

join

public class Test {
	
	class ThreadA extends Thread{
        @Override
        public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1执行完成");
            }
    }

	public static void main(String[] args) throws InterruptedException {
		Test threadClass = new Test();
		ThreadA threadA = threadClass.new ThreadA();
		threadA.start();

		threadA.join();

        //threadA 线程结束之后,最后这句才会执行,打印主线程名称
        System.out.println("线程1执行完成,主线程"+Thread.currentThread().getName()+"继续执行");

	}

}

从上面的代码中,当执行到 threadA.join() 的时候,(main)主线程会变成 WAITING 状态,直到线程 threadA 执行完毕,主线程才会变回 RUNNABLE 状态,继续往下执行。

因此状态图如下:

在这里插入图片描述
而 join 阻塞主线程就是通过 wait 和 notifyAll 实现的,我们下面打开 join 的源码看个究竟:

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
        //这里的this.isAlive,判断条件是子线程是否活着,如果活着,当前执行线程主线程就 wait 阻塞。
            while (isAlive()) {
                wait(0);
            }
        } else {
              //这里的this.isAlive,判断条件是子线程是否活着,如果活着,当前执行线程主线程就 wait 阻塞。
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从源码中,可以看到 join 是同步方法,锁对象是当前对象即 threadA。所以这里是当前线程(main 线程,因为是 main 中调用的 join 方法)持有了 threadA 对象。isAlive() 是本地方法,非同步。然后判断的是 threadA 线程是否存活。当 threadA 线程活着时,调用wait(0) 方法,这是 Object 类的方法,Object 是超类,所以这里是使持有 threadA 对象的线程阻塞,即 main 线程阻塞。

从 RUNNABLE 到 WAITING,就和执行了 wait() 方法完全一样的,那么从 WAITING 回到 RUNNABLE 是怎么实现的呢?

就是被阻塞的主线程是如何被唤醒的呢?当然是线程 threadA 结束后,由 jvm 自动调用 t.notifyAll() 唤醒主线程。

那么怎么证明?

当子线程执行完 run 方法之后,底层在 jvm 源码里,会执行线程的 exit 方法,里面会调用 notifyAll 方法。

hotspot/src/share/vm/runtime/thread.cpp
void JavaThread::exit(...) {
  ...
  ensure_join(this);
  ...
}
static void ensure_join(JavaThread* thread) {
  ...
  lock.notify_all(thread);
  ...
}

虚拟机在一个线程的方法执行完毕后,执行了个 ensure_join 方法,这个就是专门为 join 而设计的。一步步跳进方法中发现一段关键代码,lock.notify_all,这便是一个线程结束后,会自动调用自己的 notifyAll 方法的证明。

在这里我们可以总结一点: join 就是 wait,线程结束就是 notifyAll。

在这里插入图片描述

LockSupport.park()/LockSupport.unpark(Thread)

理解了上面 wait 和 notify 的机制,下面就好理解了。

如果一个线程调用 LockSupport.park() 方法,则该线程状态会从 RUNNABLE 变成 WAITING。

另一个线程调用 LockSupport.unpark(Thread) ,则刚刚的线程会从 WAITING 回到 RUNNABLE。

变化的状态图如下:
在这里插入图片描述

TIMED_WAITING(超时等待状态)

这部分就再简单不过了,超时等待与等待状态一样,唯一的区别就是将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数。

处于超时等待状态中的线程不会被分配 CPU 执行时间,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺 CPU 使用权,将超时等待状态的线程转换为运行状态。例如,调用了 wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用 notify() 或者 notifyAll() 方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。

变化的状态图如下:
在这里插入图片描述

以上就是整个线程状态转换图,到此线程状态转换全部讲解完。

总结

线程状态转换是写好多线程代码的基础,写这篇文章目的是能够更加清晰的理解线程状态转换,希望对小伙伴有所帮助,以后面试被问到可以吊打面试官。

最后来一张简化的线程转换图:

在这里插入图片描述

  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 程序是指一组指令的集合,它们被编写成一段可执行的代码。进程是正在执行的程序的实例,它是计算机资源分配的基本单位。线程是进程中的一个执行单元,它负责执行进程中的一部分任务,可以看作是轻量级的进程。线程和进程都是操作系统中的概念,程序则是更高层次的概念。 2. Java中的线程线程对象、线程优先级、线程状态线程组、线程名字等组成部分。 3. Java中提供了两创建线程的方式,一是继承Thread类,另一是实现Runnable接口。创建线程的方法是通过调用Thread类或Runnable接口的start()方法来启动线程。 4. Java中的线程生命周期包括新建状态、就绪状态、运行状态、阻塞状态和死亡状态状态间的转化过程是:新建状态 -> 就绪状态 -> 运行状态 -> 阻塞状态 -> 就绪状态 -> 运行状态 -> …… -> 死亡状态。 5. 线程状态转换的常用方法及其效果: - sleep(): 使当前线程暂停指定的时间,进入阻塞状态,不会释放锁。 - yield(): 让出当前线程的CPU时间片,让其他线程有机会执行,但不会释放锁。 - wait(): 使当前线程进入阻塞状态,直到其他线程调用notify()或notifyAll()方法来唤醒它,同时会释放锁。 - notify(): 唤醒一个正在等待该对象锁的线程,使其进入就绪状态。 - notifyAll(): 唤醒所有正在等待该对象锁的线程,使它们进入就绪状态。 6. Java中的线程同步机制是为了避免多个线程同时访问共享资源而产生的数据不一致问题。实现同步方法的方式包括synchronized关键字和Lock接口,其中synchronized关键字是Java提供的简单易用的同步机制,它可以修饰方法和代码块,保证在同一时间只有一个线程可以访问被synchronized修饰的代码段。 7. 死锁是指两个或多个线程在互相等待对方释放资源的情况下,都无法继续执行下去的状态。死锁产生的原因通常是两个或多个线程都在等待对方先释放资源才能继续执行,而没有机制来打破这僵局。 8. 线程通信是指多个线程之间的协作,以完成特定的任务。Java中的线程通信主要方法是wait()、notify()和notifyAll(),它们用于实现线程之间的同步和互斥。wait()方法使当前线程进入等待状态,直到其他线程调用notify()或notifyAll()方法来唤醒它;notify()和notifyAll()方法用于唤醒等待的线程,使它们进入就绪状态并竞争锁。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值