关于java:面试官问为什么-Java-线程没有Running状态我懵了

36次阅读

共计 5057 个字符,预计需要花费 13 分钟才能阅读完成。

送大家以下 java 学习材料





Java 虚拟机层面所裸露给咱们的状态,与操作系统底层的线程状态是两个不同层面的事。具体而言,这里说的 Java 线程状态均来自于 Thread 类下的 State 这一外部枚举类中所定义的状态:

什么是 RUNNABLE?

间接看它的 Javadoc 中的阐明:

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

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

注:这里的过程指晚期的单线程 过程,这里所谓过程状态本质就是线程状态。

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

与传统的 ready 状态的区别

更具体点,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 状态的局部细分状态,在前面咱们将会看到这一点。

与传统的 running 状态的区别

有人常感觉 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 阻塞时

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

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

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

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

当然了,咱们所谓阻塞只是指这段时间 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 状态:

比方设置一个 10ms 的倒计时,工夫一到就发一个中断,如同大限已到一样,而后重置倒计时,如此循环。

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

当初咱们再看一下 Java 中定义的线程状态,嘿,它也有 BLOCKED(阻塞),也有 WAITING(期待),甚至它还更细,还有 TIMED\_WAITING:

当初问题来了,进行阻塞式 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);`
`}`

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

网络阻塞时同理,比方 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);`
`}`

监控显示:

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

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

如何对待 RUNNABLE 状态?

首先还是后面说的,留神分清两个层面:

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

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

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

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

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

你用嘴,用手,还是用什么鸟货色来满足它的需要,它并不关怀~

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

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

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

后面咱们也强调了“Java 线程状态的扭转通常只与本身显式引入的机制无关”,如果 JVM 中的线程状态产生扭转了,通常是本身机制引发的。

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

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

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

正文完
 0