前言
线程是 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() 方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。
变化的状态图如下:
以上就是整个线程状态转换图,到此线程状态转换全部讲解完。
总结
线程状态转换是写好多线程代码的基础,写这篇文章目的是能够更加清晰的理解线程状态转换,希望对小伙伴有所帮助。
最后来一张简化的线程转换图:
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论