本人在《线程生命周期 & 中断机制》一文中提到,可以使用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版)介绍了系统中每个进程/线程都处于下列五种状态的一种:
- TASK_RUNNING:或者正在执行,或者在运行队列中等待执行;
- TASK_INTERRUPTIBLE:进程睡眠,等待条件,可接受信号并被其提前唤醒;
- TASK_UNINTERRUPTIBLE:进程睡眠,不响应信号;
- __TASK_TRACED:被其它进程跟踪;
- __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方法:
|
|
继续追踪/sun/misc/VM.java的sun.misc.VM.toThreadState():
|
|
其中JVMTI开头的静态字段在VM.java中都已经写定:
|
|
这个值和HotSpot虚拟机内的设定是一致的,不过虚拟机提供了更多的可选状态:
|
|
也许一开始会疑惑,虚拟机多出来的状态值怎么体现在toThreadState运算出的状态中呢?其实threadStatus不是单个状态的值,而是多个状态的叠加值,由于最终会使用位运算,虽然只是将状态值简单叠加,但是各位置上的0或1互不干预,比如PARKED,当在toThreadState方法中进行位运算的时候,会得到WAITING的结果,在javaClasses.hpp下查看枚举类型threadStatus,状态值叠加方式如下:
|
|
这也是为什么在程序中调用LockSupport.park()后,通过getState()得到的状态是waiting的原因。
不过以上只是简单的探索,还不清楚虚拟机各状态划分的边界在哪
,也不清楚它和操作系统线程的状态如何对标
。
更多的问题
看一个有趣的例子:
|
|
运行这段代码会输出:
|
|
然后等待用户输入,我随便打了两次ASDA,完整输出如下:
|
|
这段代码有趣的地方在于:
- 在内置锁锁定的块中,会发生了IO阻塞等待输入,但当t1线程启动,经过两秒后调用getState方法,结果显示t1处于RUNNABLE状态;
- t2启动后,立马调用getState方法,显示t2处于RUNNABLE状态;
- 在t2启动两秒后,再次调用getState方法,显示t2处于BLOCKED状态;
第1点就印证了上一节给出的文档中的注释,即虚拟机的线程状态并不反映操作系统的线程状态,IO阻塞时,在操作系统中线程会处于阻塞状态,但是上面的例子显示虚拟机线程处于RUNNABLE状态;
产生第2、3点差异的原因会在后续文章分析。
小结
同样是“勉强”,日文和中文的意思就完全不一样,而在学习Java多线程技术的过程中,“阻塞”一词,在不同层面也各有各的深意。本文的目的在于厘清一些基本概念,并抛出一些问题,在后续的解析中,将会尝试解决这些问题。
参考
JDK文档:java.lang Enum Thread.State
Linux内核设计与实现(第三版):Linux内核设计与实现(原书第3版)