Java 线程状态之 TIMED_WAITING

article/2025/8/27 6:18:48

image

定义

一个正在限时等待另一个线程执行一个动作的线程处于这一状态。

A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.

更详细的定义还是看 javadoc(jdk8):

带指定的等待时间的等待线程所处的状态。一个线程处于这一状态是因为用一个指定的正的等待时间(为参数)调用了以下方法中的其一:

  • Thread.sleep
  • 带时限(timeout)的 Object.wait
  • 带时限(timeout)的 Thread.join
  • LockSupport.parkNanos
  • LockSupport.parkUntil

对应的英文原文如下:

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:

  • Thread.sleep
  • Object.wait with timeout
  • Thread.join with timeout
  • LockSupport.parkNanos
  • LockSupport.parkUntil

不难看出,TIMED_WAITING 与 WAITING 间的联系还是很紧密的,主要差异在时限(timeout)参数上。

另外则是 sleep 这一点上的不同。

timed_waiting 的场景

实际上,在上一篇章中谈到的没有参数的 wait() 等价于 wait(0),而 wait(0) 它不是等0毫秒,恰恰相反,它的意思是永久的等下去,到天荒地老,除非收到通知。

具体可见 java 的源代码及相应 javadoc,注意:同时又还存在一种特殊的情况,所谓的“spurious wakeup”(虚假唤醒),我们在下面再讨论。

即是把自己再次活动的命运完全交给了别人(通知者),那么这样会存在什么问题呢?

在这里,我们还是继续上一篇章中的谈到的车厢场景,如不清楚的参见 Java 线程状态之 WAITING。

设想一种情况,乘务员线程增加了厕纸,正当它准备执行 notify 时,这个线程因某种原因被杀死了(持有的锁也随之释放)。这种情况下,条件已经满足了,但等待的线程却没有收到通知,还在傻乎乎地等待。

简而言之,就是存在通知失效的情况。这时,如果有个心机婊线程,她考虑得比较周全,她不是调用 wait(),而是调用 wait(1000),如果把进入 wait set 比喻成在里面睡觉等待。那么 wait(1000)相当于自带设有倒计时 1000 毫秒的闹钟,换言之,她在同时等待两个通知,并取决于哪个先到:

  • 如果在1000毫秒内,她就收到了乘务员线程的通知从而唤醒,闹钟也随之失效;
  • 反之,超过1000毫秒,还没收到通知,则闹钟响起,此时她则被闹钟唤醒。

这种情况类似于双保险。下面是一个动态的 gif 示意图(空的电池代表条件不满足,粉色的乘务员线程负责增加纸张,带有闹钟的乘客线程代表限时等待):

这样,在通知失效的情况下,她还是有机会自我唤醒的,进而完成尿尿动作。

可见,一个线程,她带不带表(闹钟),差别还是有的。其它死心眼的线程则等呀等,等到下面都湿了却依旧可能等不来通知。用本山大叔的话来说:那憋得是相当难受。

以下代码模拟了上述情形,这次,没有让乘务员线程执行通知动作,但限时等待的线程2还是自我唤醒了:

@Test
public void testTimedWaitingState() throws Exception {class Toilet { // 厕所类int paperCount = 0; // 纸张public void pee() { // 尿尿方法try {Thread.sleep(21000);// 研究表明,动物无论大小尿尿时间都在21秒左右} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}Toilet toilet = new Toilet();// 一直等待的线程1Thread passenger1 = new Thread(new Runnable() {public void run() {synchronized (toilet) {while (toilet.paperCount < 1) {try {toilet.wait(); // 条件不满足,等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}toilet.paperCount--; // 使用一张纸toilet.pee();}}});// 只等待1000毫秒的线程2Thread passenger2 = new Thread(new Runnable() {public void run() {synchronized (toilet) {while (toilet.paperCount < 1) {try {toilet.wait(1000); // 条件不满足,但只等待1000毫秒} catch (InterruptedException e) {Thread.currentThread().interrupt();}}toilet.paperCount--; // 使用一张纸toilet.pee();}}});// 乘务员线程Thread steward = new Thread(new Runnable() {public void run() {synchronized (toilet) {toilet.paperCount += 10;// 增加十张纸// 粗心的乘务员线程,没有通知到,(这里简单把代码注释掉来模拟)// toilet.notifyAll();// 通知所有在此对象上等待的线程}}});passenger1.start();passenger2.start();// 确保已经执行了 run 方法Thread.sleep(100);// 没有纸,两线程均进入等待状态,其中,线程2进入 TIMED_WAITINGassertThat(passenger1.getState()).isEqualTo(Thread.State.WAITING);assertThat(passenger2.getState()).isEqualTo(Thread.State.TIMED_WAITING);// 此时的纸张数应为0assertThat(toilet.paperCount).isEqualTo(0);// 乘务员线程启动steward.start();// 确保已经增加纸张Thread.sleep(100);// 此时的纸张数应为10assertThat(toilet.paperCount).isEqualTo(10);// 确保线程2已经自我唤醒Thread.sleep(1000);// 如果纸张已经被消耗一张,说明线程2已经成功自我唤醒assertThat(toilet.paperCount).isEqualTo(9);}

虚假唤醒(spurious wakeup)

虽然,前面说到没有参数的 wait() 等价于 wait(0),意思是永久的等下去直到被通知到。但事实上存在所谓的 “spurious wakeup”,也即是“虚假唤醒”的情况,具体可见 Object.wait(long timeout) 中的 javadoc 说明:

A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied.

一个线程也能在没有被通知、中断或超时的情况下唤醒,也即所谓的“虚假唤醒”,虽然这点在实践中很少发生,应用应该检测导致线程唤醒的条件,并在条件不满足的情况下继续等待,以此来防止这一点。

换言之,wait 应该总是在循环中调用(waits should always occur in loops),javadoc 中给出了样板代码:

synchronized (obj) {while (<condition does not hold>)obj.wait(timeout);... // Perform action appropriate to condition}

简单讲,要避免使用 if 的方式来判断条件,否则一旦线程恢复,就继续往下执行,不会再次检测条件。由于可能存在的“虚假唤醒”,并不意味着条件是满足的,这点甚至对简单的“二人转”的两个线程的 wait/notify 情况也需要注意。

另外,如果对于更多线程的情况,比如“生产者和消费者”问题,一个生产者,两个消费者,更加不能简单用 if 判断。因为可能用的是 notifyAll,两个消费者同时起来,其中一个先抢到了锁,进行了消费,等另一个也抢到锁时,可能条件又不满足了,所以还是要继续判断,不能简单认为被唤醒了就是条件满足了。

关于此话题的更多信息,可参考:

  • Doug Lea 的 《Concurrent Programming in Java (Second Edition)》3.2.3 节。
  • Joshua Bloch 的 《Effective Java Programming Language Guide》,“Prefer concurrency utilities to  wait and  notify”章节。

sleep 时的线程状态

进入 TIMED_WAITING 状态的另一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系,当然,依旧可以将它视作为一种特殊的 wait/notify 情形。

这种情况下就是完全靠“自带闹钟”来通知了。

另:sleep(0) 跟 wait(0) 是不一样的,sleep 不存在无限等待的情况,sleep(0) 相当于几乎不等待。

需要注意,sleep 方法没有任何同步语义。通常,我们会说,sleep 方法不会释放锁。

javadoc中的确切说法是:The thread does not lose ownership of any monitors.(线程不会失去任何 monitor 的所有权)

而较为夸张的说法则是说 sleep 时会抱住锁不放,这种说法不能说说错了,但不是很恰当。

打个不太确切的比方,就好比你指着一个大老爷们说:“他下个月不会来大姨妈”,那么,我们能说你说错了吗?但是,显得很怪异。

就锁这个问题而言,确切的讲法是 sleep 是跟锁无关的。

JLS 中的说法是“It is important to note that neither  Thread.sleep nor  Thread.yield have any
synchronization semantics”。(sleep 和 yield 均无任何同步语义),另一个影响是,在它们调用的前后都无需关心寄存器缓存与内存数据的一致性(no flush or reload)

见《The Java Language Specification Java SE 7 Edition》17.3 Sleep and Yield

所以,如果线程调用 sleep 时是带了锁,sleep 期间则锁还为线程锁拥有。

比如在同步块中调用 sleep(需要特别注意,或许你需要的是 wait 的方法!)

反之,如果线程调用 sleep 时没有带锁(这也是可以的,这点与 wait 不同,不是非得要在同步块中调用),那么自然也不会在sleep 期间“抱住锁不放”。

压根就没有锁,你让它抱啥呢?而 sleep 君则完全是一脸懵逼:“锁?啥是锁?我没听过这玩意!”

带 timeout 的 join 的情景与 wait(timeout) 原理类似,这里不再展开叙述。

LockSupport.parkNanos 和 parkUnitl 也交由读者自行分析。

BLOCKED 和 WAITING 状态的区别和联系

在说完了 BLOCKED,WAITING 和 TIMED_WAITING 后,我们可以综合来看看它们,比如,阻塞与等待到底有什么本质的区别呢?

显然,BLOCKED 同样可以视作是一种特殊的,隐式的 wait/nofity 机制。等待的条件就是“有锁还是没锁”。

不过,这是一个不确定的等待,可能等待(无法获取锁时),也可能不等待(能获取锁)。陷入这种阻塞后也没有自主退出的机制。

有一点需要注意的是,BLOCKED 状态是与 Java 语言级别的 synchronized 机制相关的,我们知道在 Java 5.0 之后引入了更多的机制(java.util.concurrent),除了可以用 synchronized 这种内部锁,也可以使用外部的显式锁。

显式锁有一些更好的特性,如能中断,能设置获取锁的超时,能够有多个条件等,尽管从表面上说,当显式锁无法获取时,我们还是说,线程被“阻塞”了,但却未必是 BLOCKED 状态。

当锁可用时,其中的一个线程会被系统隐式通知,并被赋予锁,从而获得在同步块中的执行权。

显然,等待锁的线程与系统同步机制形成了一个协作关系。

对比来看, WAITING 状态属于主动地显式地申请的阻塞,BLOCKED 则属于被动的阻塞,但无论从字面意义还是从根本上来看,并无本质的区别。

在前面我们也已经说过,这三个状态可以认为是传统 waiting 状态在 JVM 层面的一个细分。

总结

最后,跟传统进(线)程状态划分的一个最终对比:

image

关于 Java 线程状态的所有分析就到此为止。


http://chatgpt.dhexx.cn/article/fIYVGguc.shtml

相关文章

jstack 线程状态

转自 http://www.cnblogs.com/nexiyi/p/java_thread_jstack.html jstack 线程状态 jstack 线程里&#xff0c;值得关注的线程状态有&#xff1a; 死锁&#xff0c;Deadlock&#xff08;重点关注&#xff09; 执行中&#xff0c;Runnable 等待资源&#xff0c;Waiting on co…

Java 中线程状态有哪些?

写在前面 本文隶属于专栏《100个问题搞定Java并发》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和文献引用请见100个问题搞定Java并发 解答 线程的状态在java中有明确…

理解Java线程状态(6种,6种,6种)

什么是线程 线程具有许多传统进程所具有的特征&#xff0c;故又称为轻型进程(Light—Weight Process)或进程元&#xff1b;而把传统的进程称为重型进程(Heavy—Weight Process)&#xff0c;它相当于只有一个线程的任务。在引入了线程的操作系统中&#xff0c;通常一个进程都有…

图文详解jvm中的线程状态

本文使用下面这张图详细介绍JAVA线程的六种状态 JAVA线程的六种状态详解 在java.lang.Thread类中&#xff0c;定义了线程的以下六种状态(同一个时刻线程只能有一种状态) NEW&#xff08;新建&#xff09; 这个状态是指线程刚创建&#xff0c;但还未调用线程的start()方法进…

python封装线程类(启动、终止、查看线程状态)

文章目录 一、简单说明二、实现步骤三、测试 一、简单说明 将启动、终止和查看线程状态的方法封装成类声明时传入要启动的方法通过 start、stop 和 state 执行启动、终止 和 查看状态 二、实现步骤 # encoding: utf-8import time import threading import inspect import ct…

一文搞懂线程世界级难题——线程状态到底是6种还是5种!!!

背景 先来解答一个世界级难题&#xff1a; java线程有多少种状态&#xff1f; 答案是6种&#xff01;&#xff01;&#xff01; 那为什么有的地方说是5种呢&#xff0c;那这一定是将操作系统层面的线程状态搞混了。 下面我们就分别介绍一下java线程的6种状态以及操作系统层…

Java线程状态

线程跟人类一样拥有自己的生命周期&#xff0c;一条线程从创建到执行完毕的过程即是线程的生命周期&#xff0c;此过程可能在不同时刻处于不同的状态&#xff0c;线程状态正是这小节的主题&#xff0c;线程到底有多少种状态&#xff1f;不同状态之间是如何转化的&#xff1f; …

java线程状态与操作系统线程状态的关系

清楚的理解和认知线程状态是java多线程的基础&#xff0c;多线程本质上其实就是管理多个线程的状态&#xff0c;以期在保证线程安全的情况下获得最佳的运行效率&#xff08;发挥cpu的最佳效能&#xff09; 首先列举几个容易混淆的线程状态问题文末进行解答&#xff1a; 1Java线…

Java 如何获取线程状态呢?

下文笔者讲述Java中获取线程状态的方法分享,如下所示: java线程状态的简介 Java中的线程整个生命周期中分为5种状态:1.新建状态(New):新建的线程都为此状态2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法该状态的线程在"可运行线程池"中,…

【线程】详解线程状态(到底是五种还是六种)

首先我们要知道&#xff0c;在传统&#xff08;操作系统&#xff09;的线程模型中线程被分为五种状态 在java线程中&#xff0c;线程被分为六种状态 传统线程模型&#xff08;操作系统&#xff09;中线程状态 线程的五种状态&#xff1a; 1.新建&#xff08;new&#xff09; 创…

Java线程状态RUNNABLE详解

Java虚拟机层面所暴露给我们的状态&#xff0c;与操作系统底层的线程状态是两个不同层面的事。具体而言&#xff0c;这里说的 Java 线程状态均来自于 Thread 类下的 State 这一内部枚举类中所定义的状态&#xff1a; 什么是 RUNNABLE&#xff1f; 直接看它的 Javadoc 中的说明…

Java多线程 - 线程状态

线程状态 五个状态&#xff1a;新生、就绪、运行、死亡、阻塞 停止线程 不推荐使用JDK提供的stop()、destroy()方法【已弃用】推荐线程自己停止建议用一个标志位进行终止变量&#xff0c;到flagfalse&#xff0c;则终止线程运行 public class StopDemo implements Runnab…

jstack线程状态分析

使用jstack pid命令可以查看JVM的线程状态,其中值得关注的线程状态有&#xff1a; 死锁&#xff0c;Deadlock&#xff08;重点关注&#xff09;执行中&#xff0c;Runnable等待资源&#xff0c;Waiting on condition&#xff08;重点关注&#xff09;等待获取监视器&#xff0…

多线程之线程状态

## 线程状态 五大状态 1.创建状态&#xff1a;Thread thread new Thread(); 线程对象一旦创建就进入了新生状态。 2.就绪状态&#xff1a;当调用start()方法时&#xff0c;进入就绪状态&#xff0c;但不代表立即调度执行&#xff08;等待cpu调度&#xff09;。 3.运行状态或同…

Java线程的6种状态及切换(透彻讲解)

Java中线程的状态分为6种。 1. 初始(NEW)&#xff1a;新创建了一个线程对象&#xff0c;但还没有调用start()方法。 2. 运行(RUNNABLE)&#xff1a;Java线程中将就绪&#xff08;ready&#xff09;和运行中&#xff08;running&#xff09;两种状态笼统的称为“运行”。 线程对…

线程的5种状态详解

概念 1.初始状态(NEW)&#xff1a;新创建了一个线程对象。 2.可运行状态(RUNNABLE)&#xff1a;线程对象创建后&#xff0c;其他线程(比如main线程&#xff09;调用了该对象的start()方法。该状态的线程位于可运行线程池中&#xff0c;等待被线程调度选中&#xff0c;获取cpu…

线程的五种状态

1.新建状态&#xff08;New&#xff09;&#xff1a; 创建一个新的线程对象。 2.就绪状态&#xff08;Runnable&#xff09;: 线程创建对象后&#xff0c;其他线程调用start()方法&#xff0c;该线程处于就绪状态&#xff0c;资源已经准备就绪&#xff0c;等待CPU资源。 3.…

线程的几种状态

目录 前言 一、线程是什么&#xff1f; 二、线程状态 1.新建状态&#xff08;New&#xff09; 2.就绪状态&#xff08;Runnable&#xff09; 3.运行状态&#xff08;Running&#xff09; 4.阻塞状态&#xff08;Blocked&#xff09; 5.等待状态/超时等待&#xff08;Wa…

Java的6种线程状态以及线程状态的转换

详细介绍了Java线程的6中状态&#xff0c;以及状态之间的转换。 文章目录 1 线程状态(生命周期)1.1 源码中的状态1.2 状态解释 2 线程状态转换2.1 进入等待/超时等待2.1.1 进入等待状态2.1.1.1 wait方法的介绍2.1.1.2 join方法的介绍 2.1.2 进入超时等待2.1.2.1 sleep方法的介绍…

Lombok 的 @Builder 的使用,默认值的设置,修改属性值

1&#xff0c;简单使用 2&#xff0c;属性默认值的设置问题 3&#xff0c;修改属性值 1&#xff0c;简单使用 他这个Builder注解&#xff0c;相比之前的编辑器自动生成的getter setter的优点在哪呢&#xff1f; 看下面的使用例子 package com.lxk.lombok;import com.lxk.m…