Java
领域实现并发程序的手段就是多线程,线程是操作系统的一个概念。不同的语言如 Java
C#
等对其进行封装,但是底层调用的支持还是操作系统提供的。Java 语言里的线程本质就是操作系统的线程,它们是之间是对应的。
在操作系统层,线程也有自己的生命周期,对于生命周期,只需要搞懂各个节点的状态转换机制。
通用的线程生命周期
通用线程生命周期基本可以用五个状态来描述:
初始
状态可运行
状态运行
状态休眠
状态终止
状态
初始状态
:线程被创建,但是不允许分配 CPU 执行。该状态属于编程语言中特有状态,这里的被创建指的是在语言层面
被创建,而在操作系统
中并没有
创建真正的线程
。可运行状态
:线程可以分配CPU
执行,此时操作系统层面的线程被真正创建,可以分配 CPU 时间片。运行状态
:获得CPU
时间片后的线程为运行状态。休眠状态
:运行状态的线程如果调用了一个阻塞的 API
(例如以阻塞方式读文件)或者等待某个事件
(例如条件变量),那么线程的状态就会变成休眠状态
,同时**释放
** CPU 使用权。 休眠状态的线程永远不会获得CPU 使用权
,只有当等待事件出现,线程才会从休眠状态转为可运行状态。- 线程
执行完
或者出现异常
就会进入终止状态
,终止状态的线程不会再变为其他任何状态。
这五种状态在不同的编程语言中会有简化合并
,例如 C 语言
中的 POSIX Thread
规范 将 初始状态和可运行状态进行了合并; Java
中将 可运行状态 和 运行状态 合并,这两个状态在操作系统调度
层面有用,但是在 JVM
层面不关心这两个状态,因为 JVM
将线程调度交给操作系统处理。
除了状态合并,也可能状态细化,Java
就细化
了 休眠
状态。
Java 中的线程状态:
NEW
(初始化)RUNNABLE
(可运行/运行中)BLOCKED
(阻塞)WAITING
(无限期等待)TIMED_WAITING
(限时等待)TERMINATED
(终止)
其中 BLOCKE
、WAITING
、TIMED_WAITING
在操作系统层面是一种状态 —— 休眠
状态,只要 Java 线程处于这三种状态其中之一,都是无法获得 CPU 时间片的。
1. RUNNABLE ——> BLOCKED
只有一种情况
会导致这个转换的发生:线程等待 synchronized
锁。 synchronized
修饰的方法、代码块 同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待获取锁的线程就会从 RUNNABLE
转为 BLOCKED
状态、。 当线程获取到 synchronized
隐式锁后,状态从 BLCOKED
变回 RUNNABLE
。
在操作系统层面
,线程调用 阻塞 API
时,会转到 BLCOKED
状态,但是在 JVM 层面
, Java 的线程状态不会发生变化,仍然是 RUNNABLE
。 因为 JVM 并不关心操作系统调度相关的状态,在 JVM
来看,等待 CPU
使用权(操作系统层此时线程处于可执行状态)与等待 I/O
(操作系统层此时线程处于休眠状态)没有区别,都是等待某个资源,所以在JVM
中都归入了 RUNNABLE
状态。
我们平时说的 Java 调用 阻塞式 API 时,线程会阻塞,指的是操作系统线程的状态,不是 Java 线程的状态。
2. RUNNABLE ——> WAITING
有三种情况
会触发这种状态转换。
- 获得
synchronized
隐式锁的线程调用 无参Object.wait()
方法。 - 调用无参
Thread.join
方法,join
是一种 线程同步 方法:例如有一个线程对象thread A
,当调用A.join()
的时候,执行这条语句的线程 会等待
thread A
执行完 ,等待中的这个线程
状态会从RUNNABLE
变为WAITING
,当A
执行完,等待线程又从WAITING
转为RUNNABLE
。 - 调用
LockSupport.park,
Java 并发包中的锁都是基于LockSupport
实现的。 调用LockSupport.part
方法,当前线程会阻塞
,线程状态会从RUNNABLE
转为WAITING
,调用LockSupport.unpark(Thread thread)
可唤醒目标线程,将其状态从WAITING
转为RUNNABLE
。
RUNNABLE ——> TIMED_WAITING
有五种情况触发:
- 调用带超时参数的
Thread.sleep(long millils)
- 获得
synchronized
隐式锁的线程调用带超时参数的wait
方法 - 调用带超时参数的
join
方法 - 调用带超时参数的
LockSupport.parkNanos
方法 - 调用带超时参数的
LockSupport.parkUntil
方法
TIME_WAITING
与 WAITING
状态的触发条件仅多了超时参数这个区别。
NEW ——> RUNNABLE
Java 中线程刚创建出来时就是 NEW 状态,创建 Thread 对象主要有两种方法:
1、 继承 Thread 类:
// 自定义线程对象
class MyThread extends Thread {
public void run() {
// 线程需要执行的代码
......
}
}
// 创建线程对象
MyThread myThread = new MyThread();
2、实现 Runnable 接口,将该类对象传入 Thread 构造函数作为入参:
// 实现Runnable接口
class Runner implements Runnable {
@Override
public void run() {
// 线程需要执行的代码
......
}
}
// 创建线程对象
Thread thread = new Thread(new Runner());
当 Thread
对象调用 start
方法后,状态从 NEW
转为 RUNNABLE
。
RUNNABLE —— TERMINATED
线程 run 方法执行完后,会自动变为 TERMINATED
状态,如果执行中有异常抛出,也会导致线程终止。
有时候我们需要强制中断线程,需要调用 Thread
中的 interrupt
方法,而不要使用已经被标记为 @Deprecated
的 stop
方法。
stop 和 interrup 之间的主要区别
stop()
方法会直接杀死线程,如果线程持有 ReentrantLock
锁,被 stop
的线程不会
自动调用 ReentrantLock
的 unlock()
释放锁
,则其他线程再也无法获得 ReentrantLock
的锁,这样导致的后果极其严重,所以该方法 不再被使用。
类似的方法还有 suspend()
和 resume()
,这两个方法也不建议被使用。
interrup()
方法则是通知线程,让线程有机会继续执行后续操作
,线程也可以无视
这个关闭的通知。被 interrup
的线程有两种方法收到线程关闭通知:一种是异常
,一种是主动监测
。
例如:
当 线程A
处于 WAITING
、TIMED_WAITING
状态时,如果其他线程
调用 A
的 interrupt()
方法,会使 线程A 状态转为 RUNNABLE
,同时 线程A
的代码会触发 InterruptedException
异常。
上面提到的 WAITING
、TIMED_WAITING
状态的触发条件,都调用了类似 wait()
、join()
、sleep()
这样的方法,我们看这些方法的签名,发现都会 抛出 InterruptedException
这个异常。
这个异常的触发条件就是:其他线程调用了该线程的 interrupt()
方法。
当 线程A
处于 RUNNABLE
状态时,并且阻塞在 java.nio.channels.InterruptibleChannel
上,如果其他线程
调用 线程A
的 interrupt()
方法, 线程A
会触发 java.nio.channels.ClosedByInterruptException
这个异常。如果 线程A 阻塞在 java.nio.channels.Selector
上 ,其他线程调用了 interrupt()
,线程A
的 java.nio.channels.Selector
会立即返回。
上面两种情况属于被中断的线程通过异常
的方式获得了线程关闭的通知。 还有一种方式是主动监测
,如果线程处于 RUNNABLE
状态,并且没有阻塞
在某个 I/O
操作上,例如要中断正在计算圆周率的 线程A ,这时就需要 线程A
主动检测中断状态,线程A
可以通过 isInterrupt()
方法检测自己是否被中断。
而如果既没有异常发生,也没有主动监测,则中断请求被无视。
总结
理解 Java
线程状态
以及生命周期
对于诊断多线程 Bug 非常有用,多线程程序很难调试,非常依赖 日志、线程 dump
来跟踪问题。
分析线程 dump
的一个基本功就是分析线程状态,大部分的 死锁
、饥饿
、活锁
问题都需要跟踪分析线程的状态。
可以使用 jstack
命令 或者 Java VisualVM
可视化工具将 JVM 所有的线程栈信息导出,完整的线程栈信息不仅包括线程的当前状态,调用栈,还包括了锁的信息。
如果你的程序中存在死锁,导出的线程栈会明确告诉你发生了死锁,并且会将死锁线程的调用栈清晰地显示出来。
思考:
下面代码的意图是:线程被中断后,退出 while(true) 死循环,这段代码正确吗?
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
// 省略业务代码无数
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
分析:
如果其他线程在该线程 sleep
的时候调用 interrupt()
方法,则进入 catch块处理异常
,而这个 catch
块啥也没做,所以异常被忽略,而循环会一直继续下去。
应该改为如果进入异常,说明有线程调用 interrupt()
,在 catch
中重置 interrupt
状态:
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
// 省略业务代码无数
try {
Thread.sleep(100);
}catch (InterruptedException e){
// 将当前线程 interrupted 状态置为 true
Thread.currentThread.interrupt();
e.printStackTrace();
}
}
Q.E.D.
Comments | 0 条评论