再谈阻塞(1):从探究Java线程的状态开始

本人在《线程生命周期 & 中断机制》一文中提到,可以使用Thread类提供的getState()来获取线程状态,这些状态包括NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。而在实践中,BLOCKED似乎状态只出现在synchronized内置锁机制里。那么blocked状态是怎样一种存在?它和waiting状态的区别是什么?一切就从探究Java线程的状态开始。

需要说明的是,本系列不讨论内置锁的膨胀过程,主要研究对象是重量级锁,内置锁的优化会另开文章解析。

Linux线程状态简介

首先查看JDK文档内容java.lang Enum Thread.State,关于BLOCKED状态是这样解释的:

A thread that is blocked waiting for a monitor lock is in this state.

此状态是等待一个monitor lock时的状态,所以它基本上是一个synchronized锁机制的专属状态。

文档还说:

A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflect any operating system thread states.

意思是这些状态属于虚拟机状态,并不反映操作系统的线程状态。

那么操作系统都有那些状态?接下来就以Linux为例进行简要分析。这里要说明的是Linux对线程和进程不特别区分,线程只是一种特殊的进程。

Linux内核设计与实现(原书第3版)介绍了系统中每个进程/线程都处于下列五种状态的一种:

  1. TASK_RUNNING:或者正在执行,或者在运行队列中等待执行;
  2. TASK_INTERRUPTIBLE:进程睡眠,等待条件,可接受信号并被其提前唤醒;
  3. TASK_UNINTERRUPTIBLE:进程睡眠,不响应信号;
  4. __TASK_TRACED:被其它进程跟踪;
  5. __TASK_STOPPED:停止执行,没有投入运行也不能投入运行;

关于划分,不同的书籍介绍的可能有所不同,但是彼此间并没有根本上的差异。《Linux内核设计与实现(原书第3版)》一书的作者参与了Linux抢占式内核、进程调度器等项目的编写,在进程切换方面的理解是非常权威的,而本文通过阅读此书相关内容来理解Linux进程切换的不同状态。

Linux调度器实现的是完全公平调度算法(CFS, Completely Fair Scheduler),采用的是红黑树来组织可运行进程队列,书中这样写道:

Now let’s look at how CFS adds processes to the rbtree and caches the leftmost node. This would occur when a process becomes runnable (wakes up) or is first created via fork()…

还写道:

Waking is handled via wake_up(), which wakes up all the tasks waiting on the given wait queue. It calls try_to_wake_up(), which sets the task’s state to TASK_RUNNING, calls enqueue_task() to add the task to the red-black tree…

也就是说,进程/线程唤醒后会将其添加到红黑树中,并且会设置为TASK_RUNNING状态,等待操作系统选取运行。

可见Linux睡眠状态(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE)和执行状态(TASK_RUNNING)是以线程是否睡眠和唤醒来划分的

synchronized状态初探

我们从源码入手,首先看Thread的getState方法:

1
2
3
4
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}

继续追踪/sun/misc/VM.java的sun.misc.VM.toThreadState():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Thread.State toThreadState(int threadStatus) {
if ((threadStatus & JVMTI_THREAD_STATE_RUNNABLE) != 0) {
return RUNNABLE;
} else if ((threadStatus & JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0) {
return BLOCKED;
} else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0) {
return WAITING;
} else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0) {
return TIMED_WAITING;
} else if ((threadStatus & JVMTI_THREAD_STATE_TERMINATED) != 0) {
return TERMINATED;
} else if ((threadStatus & JVMTI_THREAD_STATE_ALIVE) == 0) {
return NEW;
} else {
return RUNNABLE;
}
}

其中JVMTI开头的静态字段在VM.java中都已经写定:

1
2
3
4
5
6
private final static int JVMTI_THREAD_STATE_ALIVE = 0x0001;
private final static int JVMTI_THREAD_STATE_TERMINATED = 0x0002;
private final static int JVMTI_THREAD_STATE_RUNNABLE = 0x0004;
private final static int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400;
private final static int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010;
private final static int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020;

这个值和HotSpot虚拟机内的设定是一致的,不过虚拟机提供了更多的可选状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface JVMTIThreadState {
public static final int JVMTI_THREAD_STATE_ALIVE = 0x0001;
public static final int JVMTI_THREAD_STATE_TERMINATED = 0x0002;
public static final int JVMTI_THREAD_STATE_RUNNABLE = 0x0004;
public static final int JVMTI_THREAD_STATE_WAITING = 0x0080;
public static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010;
public static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020;
public static final int JVMTI_THREAD_STATE_SLEEPING = 0x0040;
public static final int JVMTI_THREAD_STATE_IN_OBJECT_WAIT = 0x0100;
public static final int JVMTI_THREAD_STATE_PARKED = 0x0200;
public static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400;
public static final int JVMTI_THREAD_STATE_SUSPENDED = 0x100000;
public static final int JVMTI_THREAD_STATE_INTERRUPTED = 0x200000;
public static final int JVMTI_THREAD_STATE_IN_NATIVE = 0x400000;
}

也许一开始会疑惑,虚拟机多出来的状态值怎么体现在toThreadState运算出的状态中呢?其实threadStatus不是单个状态的值,而是多个状态的叠加值,由于最终会使用位运算,虽然只是将状态值简单叠加,但是各位置上的0或1互不干预,比如PARKED,当在toThreadState方法中进行位运算的时候,会得到WAITING的结果,在javaClasses.hpp下查看枚举类型threadStatus,状态值叠加方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum ThreadStatus {
NEW = 0,
RUNNABLE = JVMTI_THREAD_STATE_ALIVE + // runnable / running
JVMTI_THREAD_STATE_RUNNABLE,
SLEEPING = JVMTI_THREAD_STATE_ALIVE + // Thread.sleep()
JVMTI_THREAD_STATE_WAITING +
JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
JVMTI_THREAD_STATE_SLEEPING,
IN_OBJECT_WAIT = JVMTI_THREAD_STATE_ALIVE + // Object.wait()
JVMTI_THREAD_STATE_WAITING +
JVMTI_THREAD_STATE_WAITING_INDEFINITELY +
JVMTI_THREAD_STATE_IN_OBJECT_WAIT,
IN_OBJECT_WAIT_TIMED = JVMTI_THREAD_STATE_ALIVE + // Object.wait(long)
JVMTI_THREAD_STATE_WAITING +
JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
JVMTI_THREAD_STATE_IN_OBJECT_WAIT,
PARKED = JVMTI_THREAD_STATE_ALIVE + // LockSupport.park()
JVMTI_THREAD_STATE_WAITING +
JVMTI_THREAD_STATE_WAITING_INDEFINITELY +
JVMTI_THREAD_STATE_PARKED,
PARKED_TIMED = JVMTI_THREAD_STATE_ALIVE + // LockSupport.park(long)
JVMTI_THREAD_STATE_WAITING +
JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
JVMTI_THREAD_STATE_PARKED,
BLOCKED_ON_MONITOR_ENTER = JVMTI_THREAD_STATE_ALIVE + // (re-)entering a synchronization block
JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER,
TERMINATED = JVMTI_THREAD_STATE_TERMINATED
};

这也是为什么在程序中调用LockSupport.park()后,通过getState()得到的状态是waiting的原因。

不过以上只是简单的探索,还不清楚虚拟机各状态划分的边界在哪,也不清楚它和操作系统线程的状态如何对标

更多的问题

看一个有趣的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SynchronizedDemo01 {
public static void main(String[] args) {
byte[] lock = new byte[0];
Runnable task = () -> {
{
synchronized (lock) {
Scanner scanner = new Scanner(System.in);
System.out.println("waiting enter sth: ");
String s = scanner.next();
System.out.println(Thread.currentThread().getName() + ": " + s);
}
}
};
Thread t1 = new Thread(task, "t1");
Thread t2 = new Thread(task, "t2");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.getState());
t2.start();
System.out.println(t2.getState());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t2.getState());
}
}

运行这段代码会输出:

1
2
3
4
waiting enter sth:
RUNNABLE
RUNNABLE
BLOCKED

然后等待用户输入,我随便打了两次ASDA,完整输出如下:

1
2
3
4
5
6
7
8
9
waiting enter sth:
RUNNABLE
RUNNABLE
BLOCKED
ASDA
t1: ASDA
waiting enter sth:
ASDA
t2: ASDA

这段代码有趣的地方在于:

  1. 在内置锁锁定的块中,会发生了IO阻塞等待输入,但当t1线程启动,经过两秒后调用getState方法,结果显示t1处于RUNNABLE状态;
  2. t2启动后,立马调用getState方法,显示t2处于RUNNABLE状态;
  3. 在t2启动两秒后,再次调用getState方法,显示t2处于BLOCKED状态;

第1点就印证了上一节给出的文档中的注释,即虚拟机的线程状态并不反映操作系统的线程状态,IO阻塞时,在操作系统中线程会处于阻塞状态,但是上面的例子显示虚拟机线程处于RUNNABLE状态;

产生第2、3点差异的原因会在后续文章分析。

小结

同样是“勉强”,日文和中文的意思就完全不一样,而在学习Java多线程技术的过程中,“阻塞”一词,在不同层面也各有各的深意。本文的目的在于厘清一些基本概念,并抛出一些问题,在后续的解析中,将会尝试解决这些问题。

参考

JDK文档:java.lang Enum Thread.State

Linux内核设计与实现(第三版):Linux内核设计与实现(原书第3版)