Java线程生命周期
1. 操作系统线程生命周期
操作系统线程生命状态有5种。
-
初始状态(New)
进程正在被创建时的状态。仅为线程对象开辟了一块堆内存,实际上线程在操作系统层面还未创建。 -
就绪状态 (Ready)
可运行,由于其他进程处于运行状态而暂时停止运行 -
运行状态 (Running)
该进程此时正占用CPU -
阻塞状态 (Blocked)
该进程正在等待某一事件发生(如等待IO操作)而暂时停止运行,此时给它CPU控制权也无法运行 -
终止状态 (Terminated)
进程正在从系统中消失的状态
2. JVM线程生命周期
JVM线程状态有6种。
-
初始状态(New)
-
就绪/运行状态(Runnable)
-
阻塞状态(Blocked)
-
无时限等待(Waiting)
-
有时限等待(Timed_Waiting)
-
终止状态(Terminated)
其中将操作系统的就绪及运行状态合并为运行状态,将阻塞状态又细分为了阻塞状态、无时限等待、有时限等待。
2.1 线程状态切换
JVM中的线程状态切换与操作系统中有所不同。
1. New -> Runnable
创建出来的线程对象为New状态,调用start()方法便可以转为RUnnbale状态。
2. Runnable -> Blocked
JVM中,只有线程等待synchronized隐式锁,才会从Runnable转为Blocked。对于等待IO的行为,JVM依然认为是Runnable。
3. Runnable -> Waiting
-
拥有synchronized隐式锁的线程调用wait()方法
-
调用Thread实例的join()方法
-
调用LockSupport.park()方法
LockSupport.park()是Java中的一个方法,它用于暂停当前线程的执行,直到它被unpark,或者线程被中断。
LockSupport是Java并发包(java.util.concurrent)中的一个类,它提供了一些工具来帮助实现并发控制。Park方法是一个实例方法,它允许一个线程主动让出CPU的执行时间片,暂停当前线程的执行。在调用park方法后,当前线程会变为非活动状态,即暂时不会消耗CPU的时间片,直到满足某种条件(如另一个线程调用unpark方法或者线程被中断等)才会重新变为活动状态。
需要注意的是,LockSupport.park()并不会释放任何锁,也就是说,即使调用了park()方法,当前线程持有的锁也不会被自动释放。因此,在使用LockSupport.park()时,需要谨慎处理并发控制,以避免出现死锁等问题。
在实践中,LockSupport.park()常用于实现忙等待
(busy-waiting)的场景,例如在生产者-消费者问题中,当缓冲区已满时,生产者线程可以调用LockSupport.park()让出CPU的控制权,等待消费者线程消费数据后调用LockSupport.unpark()唤醒它。
虽然LockSupport.park()让出CPU的控制权,但是没有释放任何资源,所以依然被叫作忙等待。
4. Runnable -> Timed_Waiting
除了新增的Thead.sleep()方法外,其余都是Waiting方法中对应新增了超时参数的方法。
-
拥有synchronized隐式锁的线程调用wait(long timeout)方法
-
调用Thread实例的join(long millis)方法
-
调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法
-
调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
-
调用带超时参数的Thread.sleep(long millis) 方法;
5. Runnable -> Terminated
-
执行完自动进入Terminated状态
-
中断执行
使用interrupt()方法,代码中需要实现对中断状态的捕获。Waiting/Timed_Waiting状态代码都实现了捕获,会转为Runnable状态并抛出InterruptedException异常。
3. 利用线程栈信息定位
理解 Java 线程的各种状态以及生命周期对于诊断多线程 Bug 非常有帮助,多线程程序很难调试,出了 Bug 基本上都是靠日志,靠线程 dump 来跟踪问题,分析线程 dump 的一个基本功就是分析线程状态,大部分的死锁、饥饿、活锁问题都需要跟踪分析线程的状态。
可以通过 jstack 命令或者Java VisualVM这个可视化工具将 JVM 所有的线程栈信息导出来,完整的线程栈信息不仅包括线程的当前状态、调用栈,还包括了锁的信息。如下图所示。