Skip to content

JUC 线程状态

javahongxi edited this page Aug 1, 2019 · 1 revision

Java的Thread里定义了6种线程状态,下面我们来重点说说令人疑惑的RUNNABLE状态。

    // see Thread.State
    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

虚拟机线程状态 vs 操作系统线程状态

首先要明确一点,这里所谓的“Java线程状态”指的是虚拟机层面上暴露给我们的状态,这些状态是由枚举类Thread.State明确定义的。

你可能听说过这样的说法,比如在Windows系统下,很多的虚拟机实现实际都把Java线程一一映射到操作系统的**内核线程(kernel thread)**上。

除了1:1映射,还可能有N:1或者N:M映射。总之,世界很乱。。。

自然,操作系统的线程也有它自己的状态。你Java有6种,Windows可能有N种,到了Linux系统,它可能有M种。

好吧,我也不知道具体有几种~且不说操作系统各种版本满天飞。

在这里我想说的是,你管它是N种还M种!虚拟机层的存在,统一了这些差别,不管它是N还是M种,到了Java层面它们都被映射到了6种状态上来。自然,两个层面上有很多状态其实是大同小异的。至于具体差异,那是写虚拟机实现的那些家伙们去操心的事。

有可能操作系统中的两种状态在JVM中则统一成了一种状态,也可能操作系统中的一种状态在JVM中又细分成了两种状态,谁知道呢?你也不想去知道,反正我是不想去知道。

而很多关于操作系统上的书则常会提到有5种**进程(process)**状态:new,ready,running,waiting,terminated。

image

不幸的是,有很多的书上常常把这些进程状态,线程状态与Java线程状态混在一起谈。

这里所谓“进程状态”指早期的那种“单线程进程”的状态。

对于现在普遍的“多线程进程”,显然,谈论“进程状态”已经没有意义,应该谈论“进程下某个线程的状态”或者直接说“线程状态”。不过有时还是会把“进程状态”和“线程状态”混着去说。

有些系统把线程叫成“轻量级进程”(light-weight process),所以还是在谈“进程状态”。

有时则甚至既不叫“进程”,也不叫“线程”,它们叫“task”或者“job”。

总之还是有些乱的,我们不妨就拿Windows系统为例,用的就是“进程”和“线程”这两种较为标准的叫法,这时一个进程下至少有一个线程,线程是CPU调度的基本单位,进程不参与CPU调度,CPU根本不知道进程的存在。

你在“任务管理器”中看到的所谓“进程状态”,跟线程状态不是一回事。

至于Java线程的状态,有的说有4种状态,有的说有5种,各种各样的说法都有。

比如看到Java只有RUNNABLE(可运行的)状态,就觉得这还不够呀,应该还有Running(运行中)状态;

又或者觉得RUNNABLE就是Running,所以应该还有个Ready(就绪)状态才对。

然而这些说法都是不准确的!如果我们读下Thread.State源码中的注释中,它说得很清楚:

These states are virtual machine states which do not reflect any operating system thread states。

这些状态是虚拟机状态,它不反映任何操作系统的线程状态。

一个 Java 线程它所对应的操作系统内核线程中的状态可能有Running又有Ready,但在虚拟机层面则统一映射成了RUNNABLE。如果Java中觉得没必要去区分这些,我们又何必去纠结这些呢?

以RUNNABLE为例,源码中的注释是这样说的:executing in the Java virtual machine(正在Java虚拟机中执行)

至于它是否真正在执行,不是我们要操心的事。

还有的情况则比如把Java状态中的BLOCKED,WAITING,TIMED_WAITING三种状态都笼统地称为blocked或者waiting;

操作系统也许只有一种状态,但这一次,Java作了细分,给出了三种状态。

很多声称Java线程只有4种或5种状态常常都是自作主张地合并了这些状态,把这些东西混为一谈是非常容易引发混乱的。我们将会在后面具体地谈到。

又或者把TIMED_WAITING当作不存在,从来不提有这个状态。

显然,这种做法又是受到传统进程状态划分的影响。尽管它与WAITING很像,我们最好按着Thread.State中的定义来,不要自己随意发挥。

综上所述,为避免出现混乱,厘清概念所处的层次是非常重要的。如无特别说明,讨论均在JVM层面上。

具体而言,比如当说到WAITING状态时,指的就是Thread.State.WAITING。

有了以上基础,才能在接下来更好地去分析这6种状态。

接下来再来细看这6个状态,首先从简单的谈起。

当使用new Thread()创建一个新的线程,又还没有开始执行(not yet started)它的时候就处于NEW状态。

这里所谓“开始执行”具体指调用线程类中的start方法。

注意:你不能直接调用run方法,这样的话还是在原线程上执行。只有调用start方法才会开启新的执行线程,接着它会去调用run。

在start之后,线程进入RUNNABLE状态,之后还可能会继续转换成其它状态。

注:一个线程只能被start一次。

终止状态,这个也没什么好说的,完成了执行后(completed execution)或者说退出了(exited)。线程就进入了终止状态。

余下的几个状态,由于无法简单几句说完,这里先作些简介:

RUNNABLE:前面有提到,它指“正在Java虚拟机中执行”。

BLOCKED:等待监视器锁(waiting for a monitor lock )

这是一种特殊的waiting,实际上就是被synchronized方法或者块阻塞。

monitor有些书上通常叫”管程“,我也不太确定要怎么叫它。这里叫成”监视器“也是取字面的意思。

WAITING:无限期等待另一个线程执行一个特别的动作。(waiting indefinitely for another thread to perform a particular action )

这里所谓的动作通常即是指“notify或是notifyAll”。

TIMED_WAITING:限时等待另一个线程执行一个动作。(waiting for another thread to perform an action for up to a specified waiting time )

如果没有等到如“notify”之类的动作,时间到了也会自动退出这一状态。

下面详细说说RUNNABLE状态。

什么是 RUNNABLE?

直接看它的 Javadoc 中的说明:

一个在 JVM 中执行的线程处于这一状态中。(A thread executing in the Java virtual machine is in this state.)

而传统的进(线)程状态一般划分如下:

image

注:这里的进程指早期的单线程进程,这里所谓进程状态实质就是线程状态。

那么 runnable 与图中的 ready 与 running 区别在哪呢?

更具体点,javadoc 中是这样说的:

处于 runnable 状态下的线程正在 Java 虚拟机中执行,但它可能正在等待来自于操作系统的其它资源,比如处理器。

A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.

显然,runnable 状态实质上是包括了 ready 状态的。

甚至还可能有包括上图中的 waiting 状态的部分细分状态,在后面我们将会看到这一点。

有人常觉得 Java 线程状态中还少了个 running 状态,这其实是把两个不同层面的状态混淆了。对 Java 线程状态而言,不存在所谓的running 状态,它的 runnable 状态包含了 running 状态。

我们可能会问,为何 JVM 中没有去区分这两种状态呢?

现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。

更复杂的可能还会加入优先级(priority)的机制。

这个时间分片通常是很小的,一个线程一次最多只能在 cpu 上运行比如10-20ms 的时间(此时处于 running 状态),也即大概只有0.01秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)

注:如果期间进行了 I/O 的操作还会导致提前释放时间分片,并进入等待队列。

又或者是时间分片没有用完就被抢占,这时也是回到 ready 状态。

这一切换的过程称为线程的上下文切换(context switch),当然 cpu 不是简单地把线程踢开就完了,还需要把被相应的执行状态保存到内存中以便后续的恢复执行。

显然,10-20ms 对人而言是很快的,

不计切换开销(每次在1ms 以内),相当于1秒内有50-100次切换。事实上时间片经常没用完,线程就因为各种原因被中断,实际发生的切换次数还会更多。

也这正是单核 CPU 上实现所谓的“并发(concurrent)”的基本原理,但其实是快速切换所带来的假象,这有点类似一个手脚非常快的杂耍演员可以让好多个球同时在空中运转那般。

时间分片也是可配置的,如果不追求在多个线程间很快的响应,也可以把这个时间配置得大一点,以减少切换带来的开销。

如果是多核CPU,才有可能实现真正意义上的并发,这种情况通常也叫并行(pararell),不过你可能也会看到这两词会被混着用,这里就不去纠结它们的区别了。

通常,Java的线程状态是服务于监控的,如果线程切换得是如此之快,那么区分 ready 与 running 就没什么太大意义了。

当你看到监控上显示是 running 时,对应的线程可能早就被切换下去了,甚至又再次地切换了上来,也许你只能看到 ready 与 running 两个状态在快速地闪烁。

当然,对于精确的性能评估而言,获得准确的 running 时间是有必要的。

现今主流的 JVM 实现都把 Java 线程一一映射到操作系统底层的线程上,把调度委托给了操作系统,我们在虚拟机层面看到的状态实质是对底层状态的映射及包装。JVM 本身没有做什么实质的调度,把底层的 ready 及 running 状态映射上来也没多大意义,因此,统一成为runnable 状态是不错的选择。

我们将看到,Java 线程状态的改变通常只与自身显式引入的机制有关。

我们知道传统的I/O都是阻塞式(blocked)的,原因是I/O操作比起cpu来实在是太慢了,可能差到好几个数量级都说不定。如果让 cpu 去等I/O 的操作,很可能时间片都用完了,I/O 操作还没完成呢,不管怎样,它会导致 cpu 的利用率极低。

所以,解决办法就是:一旦线程中执行到 I/O 有关的代码,相应线程立马被切走,然后调度 ready 队列中另一个线程来运行。

这时执行了 I/O 的线程就不再运行,即所谓的被阻塞了。它也不会被放到调度队列中去,因为很可能再次调度到它时,I/O 可能仍没有完成。

线程会被放到所谓的等待队列中,处于上图中的 waiting 状态:

image

当然了,我们所谓阻塞只是指这段时间 cpu 暂时不会理它了,但另一个部件比如硬盘则在努力地为它服务。cpu 与硬盘间是并发的。如果把线程视作为一个 job,这一 job 由 cpu 与硬盘交替协作完成,当在 cpu 上是 waiting 时,在硬盘上却处于 running,只是我们在操作系统层面讨论线程状态时通常是围绕着 cpu 这一中心去述说的。

而当 I/O 完成时,则用一种叫中断(interrupt)的机制来通知 cpu:

也即所谓的“中断驱动(interrupt-driven)”,现代操作系统基本都采用这一机制。

某种意义上,这也是控制反转(IoC)机制的一种体现,cpu不用反复去询问硬盘,这也是所谓的“好莱坞原则”—Don’t call us, we will call you.好莱坞的经纪人经常对演员们说:“别打电话给我,(有戏时)我们会打电话给你。”

在这里,硬盘与 cpu 的互动机制也是类似,硬盘对 cpu 说:”别老来问我 IO 做完了没有,完了我自然会通知你的“

当然了,cpu 还是要不断地检查中断,就好比演员们也要时刻注意接听电话,不过这总好过不断主动去询问,毕竟绝大多数的询问都将是徒劳的。

cpu 会收到一个比如说来自硬盘的中断信号,并进入中断处理例程,手头正在执行的线程因此被打断,回到 ready 队列。而先前因 I/O 而waiting 的线程随着 I/O 的完成也再次回到 ready 队列,这时 cpu 可能会选择它来执行。

另一方面,所谓的时间分片轮转本质上也是由一个定时器定时中断来驱动的,可以使线程从 running 回到 ready 状态:

image

比如设置一个10ms 的倒计时,时间一到就发一个中断,好像大限已到一样,然后重置倒计时,如此循环。

与 cpu 正打得火热的线程可能不情愿听到这一中断信号,因为它意味着这一次与 cpu 缠绵的时间又要到头了......奴为出来难,何日君再来?

现在我们再看一下 Java 中定义的线程状态,嘿,它也有 BLOCKED(阻塞),也有 WAITING(等待),甚至它还更细,还有TIMED_WAITING:

image

现在问题来了,进行阻塞式 I/O 操作时,Java 的线程状态究竟是什么?是 BLOCKED?还是 WAITING?

可能你已经猜到,既然放到 RUNNABLE 这一主题下讨论,其实状态还是 RUNNABLE。我们也可以通过一些测试来验证这一点:

@Test
public void testInBlockedIOState() throws InterruptedException {
    Scanner in = new Scanner(System.in);
    // 创建一个名为“输入输出”的线程t
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // 命令行中的阻塞读
                String input = in.nextLine();
                System.out.println(input);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
              IOUtils.closeQuietly(in);
            }
        }
    }, "输入输出"); // 线程的名字
    
    // 启动
    t.start();
    
    // 确保run已经得到执行
    Thread.sleep(100);
    
    // 状态为RUNNABLE
    assertThat(t.getState()).isEqualTo(Thread.State.RUNNABLE);
}

在最后的语句上加一断点,监控上也反映了这一点:

image

网络阻塞时同理,比如socket.accept,我们说这是一个“阻塞式(blocked)”式方法,但线程状态还是 RUNNABLE。

@Test
public void testBlockedSocketState() throws Exception {
    Thread serverThread = new Thread(new Runnable() {
        @Override
        public void run() {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(10086);
                while (true) {
                    // 阻塞的accept方法
                    Socket socket = serverSocket.accept();
                    // TODO
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }, "socket线程"); // 线程的名字
    serverThread.start();
    
    // 确保run已经得到执行
    Thread.sleep(500);
    
    // 状态为RUNNABLE
    assertThat(serverThread.getState()).isEqualTo(Thread.State.RUNNABLE);

}

监控显示:

image

当然,Java 很早就引入了所谓 nio(新的IO)包,至于用 nio 时线程状态究竟是怎样的,这里就不再一一具体去分析了。

至少我们看到了,进行传统上的 IO 操作时,口语上我们也会说“阻塞”,但这个“阻塞”与线程的 BLOCKED 状态是两码事!

首先还是前面说的,注意分清两个层面:

image

虚拟机是骑在你操作系统上面的,身下的操作系统是作为某种资源为满足虚拟机的需求而存在的:

image

当进行阻塞式的 IO 操作时,或许底层的操作系统线程确实处在阻塞状态,但我们关心的是 JVM 的线程状态。

JVM 并不关心底层的实现细节,什么时间分片也好,什么 IO 时就要切换也好,它并不关心。

前面说到,“处于 runnable 状态下的线程正在 Java 虚拟机中执行,但它可能正在等待来自于操作系统的其它资源,比如处理器。”

JVM 把那些都视作资源,cpu 也好,硬盘,网卡也罢,有东西在为线程服务,它就认为线程在“执行”。

你用嘴,用手,还是用什么鸟东西来满足它的需求,它并不关心~

处于 IO 阻塞,只是说 cpu 不执行线程了,但网卡可能还在监听呀,虽然可能暂时没有收到数据:

就好比前台或保安坐在他们的位置上,可能没有接待什么人,但你能说他们没在工作吗?

所以 JVM 认为线程还在执行。而操作系统的线程状态是围绕着 cpu 这一核心去述说的,这与 JVM 的侧重点是有所不同的。

前面我们也强调了“Java 线程状态的改变通常只与自身显式引入的机制有关”,如果 JVM 中的线程状态发生改变了,通常是自身机制引发的。

比如 synchronize 机制有可能让线程进入BLOCKED 状态,sleep,wait等方法则可能让其进入 WATING 之类的状态。

它与传统的线程状态的对应可以如下来看:

image

RUNNABLE 状态对应了传统的 ready, running 以及部分的 waiting 状态。

首页

Java核心技术 🔥

Netty 🔥

RocketMQ深入研究 🔥

Dubbo源码导读

微服务架构

Redis

其他

杂谈

关于

Clone this wiki locally
You can’t perform that action at this time.