Java 领域实现并发程序的手段就是多线程,线程是操作系统的一个概念。不同的语言如 Java C# 等对其进行封装,但是底层调用的支持还是操作系统提供的。Java 语言里的线程本质就是操作系统的线程,它们是之间是对应的。

在操作系统层,线程也有自己的生命周期,对于生命周期,只需要搞懂各个节点的状态转换机制

通用的线程生命周期

通用线程生命周期基本可以用五个状态来描述:

  • 初始状态
  • 可运行状态
  • 运行状态
  • 休眠状态
  • 终止状态

  1. 初始状态:线程被创建,但是不允许分配 CPU 执行。该状态属于编程语言中特有状态,这里的被创建指的是在语言层面被创建,而在操作系统中并没有创建真正的线程
  2. 可运行状态:线程可以分配 CPU 执行,此时操作系统层面的线程被真正创建,可以分配 CPU 时间片。
  3. 运行状态:获得 CPU 时间片后的线程为运行状态。
  4. 休眠状态:运行状态的线程如果调用了一个阻塞的 API (例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会变成休眠状态,同时**释放** CPU 使用权。 休眠状态的线程永远不会获得 CPU 使用权,只有当等待事件出现,线程才会从休眠状态转为可运行状态。
  5. 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会再变为其他任何状态。

这五种状态在不同的编程语言中会有简化合并,例如 C 语言中的 POSIX Thread 规范 将 初始状态和可运行状态进行了合并Java 中将 可运行状态 和 运行状态 合并,这两个状态在操作系统调度层面有用,但是在 JVM 层面不关心这两个状态,因为 JVM 将线程调度交给操作系统处理。

除了状态合并,也可能状态细化,Java细化休眠状态。

Java 中的线程状态:

  1. NEW(初始化)
  2. RUNNABLE(可运行/运行中)
  3. BLOCKED(阻塞)
  4. WAITING(无限期等待)
  5. TIMED_WAITING(限时等待)
  6. TERMINATED(终止)

其中 BLOCKEWAITINGTIMED_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

三种情况会触发这种状态转换。

  1. 获得 synchronized 隐式锁的线程调用 无参 Object.wait() 方法。
  2. 调用无参 Thread.join 方法,join 是一种 线程同步 方法:例如有一个线程对象 thread A ,当调用 A.join() 的时候,执行这条语句的线程 等待 thread A 执行完 ,等待中的这个线程状态会从 RUNNABLE 变为 WAITING,当 A 执行完,等待线程又从 WAITING 转为 RUNNABLE
  3. 调用 LockSupport.park, Java 并发包中的锁都是基于 LockSupport 实现的。 调用 LockSupport.part 方法,当前线程会阻塞,线程状态会从 RUNNABLE 转为 WAITING,调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,将其状态从 WAITING 转为 RUNNABLE

RUNNABLE ——> TIMED_WAITING

有五种情况触发:

  1. 调用带超时参数的 Thread.sleep(long millils)
  2. 获得 synchronized 隐式锁的线程调用带超时参数的 wait 方法
  3. 调用带超时参数的 join 方法
  4. 调用带超时参数的 LockSupport.parkNanos 方法
  5. 调用带超时参数的 LockSupport.parkUntil 方法

TIME_WAITINGWAITING 状态的触发条件仅多了超时参数这个区别。

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 方法,而不要使用已经被标记为 @Deprecatedstop 方法。

stop 和 interrup 之间的主要区别

stop() 方法会直接杀死线程,如果线程持有 ReentrantLock 锁,被 stop 的线程不会自动调用 ReentrantLockunlock() 释放锁,则其他线程再也无法获得 ReentrantLock 的锁,这样导致的后果极其严重,所以该方法 不再被使用。

类似的方法还有 suspend()resume() ,这两个方法也不建议被使用。

interrup() 方法则是通知线程,让线程有机会继续执行后续操作,线程也可以无视这个关闭的通知。被 interrup 的线程有两种方法收到线程关闭通知:一种是异常,一种是主动监测

例如:

线程A 处于 WAITINGTIMED_WAITING 状态时,如果其他线程调用 Ainterrupt() 方法,会使 线程A 状态转为 RUNNABLE,同时 线程A 的代码会触发 InterruptedException 异常。

上面提到的 WAITINGTIMED_WAITING 状态的触发条件,都调用了类似 wait()join()sleep() 这样的方法,我们看这些方法的签名,发现都会 抛出 InterruptedException 这个异常。

这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法。

线程A 处于 RUNNABLE 状态时,并且阻塞在 java.nio.channels.InterruptibleChannel 上,如果其他线程调用 线程Ainterrupt() 方法, 线程A 会触发 java.nio.channels.ClosedByInterruptException 这个异常。如果 线程A 阻塞在 java.nio.channels.Selector 上 ,其他线程调用了 interrupt()线程Ajava.nio.channels.Selector 会立即返回。

上面两种情况属于被中断的线程通过异常的方式获得了线程关闭的通知。 还有一种方式是主动监测,如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如要中断正在计算圆周率的 线程A ,这时就需要 线程A 主动检测中断状态线程A 可以通过 isInterrupt() 方法检测自己是否被中断。

而如果既没有异常发生,也没有主动监测,则中断请求被无视。

总结

理解 Java 线程状态以及生命周期对于诊断多线程 Bug 非常有用,多线程程序很难调试,非常依赖 日志、线程 dump 来跟踪问题。

分析线程 dump 的一个基本功就是分析线程状态,大部分的 死锁饥饿活锁问题都需要跟踪分析线程的状态。

可以使用 jstack 命令 或者 Java VisualVM 可视化工具将 JVM 所有的线程栈信息导出,完整的线程栈信息不仅包括线程的当前状态,调用栈,还包括了锁的信息。

如果你的程序中存在死锁,导出的线程栈会明确告诉你发生了死锁,并且会将死锁线程的调用栈清晰地显示出来。

img

思考:

下面代码的意图是:线程被中断后,退出 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.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。