Java多线程编程实战指南:设计模式篇(第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 线程的状态与上下文切换

在Java语言中,一个线程从创建、启动到运行结束的整个生命周期可能经历若干个状态,如图1-1所示。

图1-1 Java线程的状态

Java线程的状态可以通过调用相应Thread实例的getState方法获取。该方法的返回值类型Thread.State是一个枚举类型(Enum)。Thread.State所定义的线程状态包括以下几种。

• NEW:一个刚创建而未启动的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程只可能处于该状态一次。

• RUNNABLE:该状态可以看成一个复合状态。它包括两个子状态:READY和RUNNING。前者表示处于该状态的线程可以被线程调度器(Scheduler)调度而使之处于RUNNING状态。后者表示处于该状态的线程正在运行,即相应线程对象的run方法中的代码所对应的指令正在由CPU执行。当Thread实例的yield方法被调用时或者通过线程调度器,相应线程的状态会由RUNNING转换为READY。

• BLOCKED:在一个线程发起一个阻塞式I/O(Blocking I/O)操作[5]后,或者试图获得一个由其他线程持有的锁时,相应的线程会处于该状态。处于该状态的线程并不会占用CPU资源。在相应的I/O操作完成后,或者相应的锁被其他线程释放后,该线程的状态又可以转换为RUNNABLE

• WAITING:一个线程在执行了某些方法调用之后就会处于这种无限等待其他线程执行特定操作的状态。这些方法有Object.wait、Thread.join和LockSupport.park。能够使相应线程从WAITING转换到RUNNABLE的相应方法有Object.notify、Object.notifyAll和LockSupport.unpark(thread)。

• TIMED_WAITING:该状态与WAITING状态类似,差别在于处于该状态的线程并非无限等待其他线程执行特定操作,而是处于带有时间限制的等待状态。当其他线程没有在指定时间内执行该线程所期望的特定操作时,该线程的状态将自动转换为RUNNABLE。

• TERMINATED:已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程也只可能处于该状态一次。Thread实例的run方法正常返回或者由于抛出异常而提前停止,都会导致相应线程处于该状态。

从上述描述可知,一个线程在其整个生命周期中,只可能处于NEW状态和TERMINATED状态一次。而一个线程的状态从RUNNABLE状态转换为BLOCKEDWAITING和TIMED_WAITING等状态中的任何一个状态,都意味着上下文切换(Context Switch)的产生。

上下文切换类似于我们接听手机电话的场景。比如,当我们正在接听一个电话并与对方讨论某件事情的时候,这时突然有另一个来电。通常这个时候我们会跟对方说“我先接个电话,你别挂断。”并记下与他的讨论进行到了什么程度,然后接听新的来电并告诉对方稍后会回拨并将该来电挂断,接着又继续先前的讨论。在接听新来电之前,如果我们没有特意记下当前的讨论进展到什么程度,等我们接听新来电后再回过头继续讨论时,可能得问对方“刚才我们讲到哪里了”这样的问题。

在多线程环境中,当一个线程的状态由RUNNABLE转换为非RUNNABLE(BLOCKED、WAITING或者TIMED_WAITING)时,相应线程的上下文信息(即所谓的Context,包括CPU的寄存器和程序计数器在某一时间点的内容等)需要被保存,以便相应线程稍后再次进入RUNNABLE状态时能够在之前执行进度的基础上继续前进。而当一个线程的状态由非RUNNABLE状态进入RUNNABLE状态时,可能涉及恢复之前保存的线程上下文信息并在此基础上前进。这个对线程的上下文信息进行保存和恢复的过程就被称为上下文切换。

上下文切换会带来额外的开销,包括保存和恢复线程上下文信息的开销、对线程进行调度的CPU时间开销以及CPU缓存内容失效(即CPU的L1 Cache、L2 Cache等)的开销[6]

在Linux平台下,我们可以使用perf命令来监视Java程序运行过程中的上下文切换情况。例如,我们可以使用perf命令来监视如清单1-2所示程序的运行情况,相应的命令如下:

在上述命令中,参数e的值中的cs表示被监视程序的上下文切换次数。上述命令执行后输出的内容类似下面这样:

由此可见,如清单1-2所示程序的这次运行一共产生了434次上下文切换。

在Windows平台下,可以使用Windows自带的工具perfmon[7]来监视Java程序运行过程中的上下文切换情况。