2019独角兽企业重金招聘Python工程师标准>>>

在上一篇里我们讲了一个重要状态:BLOCKED,在这一篇章里,我们来看另一个重要的状态:WAITING(等待)。

定义

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

A thread that is waiting indefinitely for another thread to perform a particular action is in this state.

然而这里并没有详细说明这个“特别的动作”到底是什么,详细定义还是看 javadoc(jdk8):

一个线程进入 WAITING 状态是因为调用了以下方法:

  • 不带时限的 Object.wait 方法
  • 不带时限的 Thread.join 方法
  • LockSupport.park

然后会等其它线程执行一个特别的动作,比如:

  • 一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify() 或 Object.notifyAll()。
  • 一个调用了 Thread.join 方法的线程会等待指定的线程结束。

对应的英文原文如下:

A thread is in the waiting state due to calling one of the following methods:

  • Object.wait with no timeout
  • Thread.join with no timeout
  • LockSupport.park

A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

线程间的协作(cooperate)机制

显然,WAITING 状态所涉及的不是一个线程的独角戏,相反,它涉及多个线程,具体地讲,这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。

就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

wait/notify 就是线程间的一种协作机制,那么首先,为什么 wait?什么时候 wait?它为什么要等其它线程执行“特别的动作”?它到底解决了什么问题?

wait 的场景

首先,为什么要 wait 呢?简单讲,是因为条件(condition)不满足。那么什么是条件呢?为方便理解,我们设想一个场景:

有一节列车车厢,有很多乘客,每个乘客相当于一个线程;里面有个厕所,这是一个公共资源,且一次只允许一个线程进去访问(毕竟没人希望在上厕所期间还与他人共享~)。

竞争关系

假如有多个乘客想同时上厕所,那么这里首先存在的是竞争的关系。

如果将厕所视为一个对象,它有一把锁,想上厕所的乘客线程需要先获取到锁,然后才能进入厕所。

Java 在语言级直接提供了同步的机制,也即是 synchronized 关键字:

synchronized(expression) {……}

它的机制是这样的:对表达式(expresssion)求值(值的类型须是引用类型(reference type)),获取它所代表的对象,然后尝试获取这个对象的锁:

  • 如果能获取锁,则进入同步块执行,执行完后退出同步块,并归还对象的锁(异常退出也会归还);
  • 如果不能获取锁,则阻塞在这里,直到能够获取锁。

在一个线程还在厕所期间,其它同时想上厕所的线程被阻塞,处在该厕所对象的 entry set 中,处于 BLOCKED 状态。

完事之后,退出厕所,归还锁。

之后,系统再在 entry set 中挑选一个线程,将锁给到它。

对于以上过程,以下为一个 gif 动图演示:

当然,这就是我们所熟悉的锁的竞争过程。以下为演示的代码:

@Test
public void testBlockedState() throws Exception {class Toilet { // 厕所类public void pee() { // 尿尿方法try {Thread.sleep(21000);// 研究表明,动物无论大小尿尿时间都在21秒左右} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}Toilet toilet = new Toilet();Thread passenger1 = new Thread(new Runnable() {public void run() {synchronized (toilet) {toilet.pee();}}});Thread passenger2 = new Thread(new Runnable() {public void run() {synchronized (toilet) {toilet.pee();}}});passenger1.start();// 确保乘客1先启动Thread.sleep(100);passenger2.start();// 确保已经执行了 run 方法Thread.sleep(100);// 在乘客1在厕所期间,乘客2处于 BLOCKED 状态assertThat(passenger2.getState()).isEqualTo(Thread.State.BLOCKED);
}

条件

现在,假设有个女乘客,她抢到了锁,进去之后裤子脱了一半,发现马桶的垫圈纸没了,于是拒绝尿。

或许是因为她比较讲究卫生,怕直接坐上去会弄脏她白花花的屁股~

现在,条件出现了:有纸没纸,这就是某种条件。

那么,现在条件不满足,这位女线程改怎么办呢?如果只是在里面干等,显然是不行的。

这不就是人民群众所深恶痛绝的“占着茅坑不拉尿”吗?

  • 一方面,外面 entry set 中可能好多群众还嗷嗷待尿呢(其中可能有很多大老爷线程,他们才不在乎有没有马桶垫圈纸~)
  • 另一方面,假定外面同时有“乘务员线程”,准备进去增加垫圈纸,可你在里面霸占着不出来,别人也没法进去,也就没法加纸。

所以,当条件不满足时,需要出来,要把锁还回去,以使得诸如“乘务员线程”的能进去增加纸张。

等待是必要的吗?

那么出来之后是否一定需要等待呢?当然也未必。

这里所谓“等待”,指的是使线程处于不再活动的状态,即是从调度队列中剔除。

如果不等待,只是简单归还锁,用一个反复的循环来判断条件是否满足,那么还是可以再次回到调度队列,然后期待在下一次被调度到的时候,可能条件已经发生变化:

比如某个“乘务员线程”已经在之前被调度并增加了里面的垫圈纸。自然,也可能再次调度到的时候,条件依旧是不满足的。

现在让我们考虑一种比较极端的情况:厕所外一大堆的“女乘客线程”想进去方便,同时还有一个焦急的“乘务员线程”想进去增加厕纸。

如果线程都不等待,而厕所又是一个公共资源,无法并发访问。调度器每次挑一个线程进去,挑中“乘务员线程”的几率反而降低了,entry set 中很可能越聚越多无法完成方便的“女乘客线程”,“乘务员线程”被选中执行的几率越发下降。

当然,同步机制会防止产生所谓的“饥饿(starvation)”现象,“乘务员线程”最终还是有机会执行的,只是系统运行的效率下降了。

所以,这会干扰正常工作的线程,挤占了资源,反而影响了自身条件的满足。另外,“乘务员线程”可能这段时间根本没有启动,此时,不愿等待的“女乘客线程”不过是徒劳地进进出出,占用了 CPU 资源却没有办成正事。

效果上还是在这种没有进展的进进出出中等待,这种情形类似于所谓的忙等待 (busy waiting)

协作关系

综上,等待还是有必要的,我们需要一种更高效的机制,也即是 wait/notify 的协作机制。

当条件不满足时,应该调用 wait()方法,这时线程释放锁,并进入所谓的 wait set 中,具体的讲,是进入这个厕所对象的 wait set 中:

这时,线程不再活动,不再参与调度,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。

现在的问题是:她们什么时候才能再次活动呢?显然,最佳的时机是当条件满足的时候。

之后,“乘务员线程”进去增加厕纸,当然,此时,它也不能只是简单加完厕纸就完了,它还要执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的女乘客线程:

大概就是向她们喊一声:“有纸啦!赶紧去尿吧!”显然,如果只是“女乘客线程”方面一厢情愿地等待,她们将没有机会再执行。

所谓“通知”,也即是把她们从 wait set 中释放出来,重新进入到调度队列(ready queue)中。

  • 如果是 notify,则选取所通知对象的 wait set 中的一个线程释放;
  • 如果是 notifyAll,则释放所通知对象的 wait set 上的全部线程。

整个过程如下图所示:

对于上述过程,我们也给出以下 gif 动图演示:

注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为她当初中断的地方是在同步块内,而此刻她已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。(这也即是所谓的 “reenter after calling Object.wait”,在上一个篇章中也曾详细的讨论了这一过程。)

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
  • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态。

综上,这是一个协作机制,“女乘客线程”和“乘务员线程”间存在一个协作关系。显然,这种协作关系的存在,“女乘客线程”可以避免在条件不满足时的盲目尝试,也为“乘务员线程”的顺利执行腾出了资源;同时,在条件满足时,又能及时得到通知。协作关系的存在使得彼此都能受益。

生产者与消费者问题

不难发现,以上实质上也就是经典的“生产者与消费者”的问题:

乘务员线程生产厕纸,女乘客线程消费厕纸。当厕纸没有时(条件不满足),女乘客线程等待,乘务员线程添加厕纸(使条件满足),并通知女乘客线程(解除她们的等待状态)。接下来,女乘客线程能否进一步执行则取决于锁的获取情况。

代码的演示:

在以下代码中,演示了上述的 wait/notify 的过程:

@Test
public void testWaitingState() 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();// 两乘客线程Thread[] passengers = new Thread[2];for (int i = 0; i < passengers.length; i++) {passengers[i] = 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();}}});}// 乘务员线程Thread steward = new Thread(new Runnable() {public void run() {synchronized (toilet) {toilet.paperCount += 10;// 增加十张纸toilet.notifyAll();// 通知所有在此对象上等待的线程}}});passengers[0].start();passengers[1].start();// 确保已经执行了 run 方法Thread.sleep(100);// 没有纸,两线程均进入等待状态assertThat(passengers[0].getState()).isEqualTo(Thread.State.WAITING);assertThat(passengers[1].getState()).isEqualTo(Thread.State.WAITING);// 乘务员线程启动,救星来了steward.start();// 确保已经增加纸张并已通知Thread.sleep(100);// 其中之一会得到锁,并执行 pee,但无法确定是哪个,所以用 "或 ||"// 注:因为 pee 方法中实际调用是 sleep, 所以很快就从 RUNNABLE 转入 TIMED_WAITING(sleep 时对应的状态)assertTrue(Thread.State.TIMED_WAITING.equals(passengers[0].getState())|| Thread.State.TIMED_WAITING.equals(passengers[1].getState()));// 其中之一则被阻塞,但无法确定是哪个,所以用 "或 ||"assertTrue(Thread.State.BLOCKED.equals(passengers[0].getState()) || Thread.State.BLOCKED.equals(passengers[1].getState()));
}

join 的场景及其它

从定义中可知,除了 wait/notify 外,调用 join 方法也会让线程处于 WAITING 状态。

join 的机制中并没有显式的 wait/notify 的调用,但可以视作是一种特殊的,隐式的 wait/notify 机制。

假如有 a,b 两个线程,在 a 线程中执行 b.join(),相当于让 a 去等待 b,此时 a 停止执行,等 b 执行完了,系统内部会隐式地通知 a,使 a 解除等待状态,恢复执行。

换言之,a 等待的条件是 “b 执行完毕”,b 完成后,系统会自动通知 a。

关于 LockSupport.park 的情况则由读者自行分析。

与传统 waiting 状态的关系

Thread.State.WAITING 状态与传统的 waiting 状态类似:

不过,Java 中还细分出了 TIMED_WAITING 状态,由于篇幅关系,我们在下一篇中再分析最后一个 TIMED_WAITING 状态。

转载于:https://my.oschina.net/goldenshaw/blog/802620

Java 线程状态之 WAITING相关推荐

  1. Java 线程状态之 TIMED_WAITING

    定义 一个正在限时等待另一个线程执行一个动作的线程处于这一状态. A thread that is waiting for another thread to perform an action fo ...

  2. Java线程状态Jstack线程状态BLOCKED/TIMED_WAITING/WAITING解释

    一.线程5种状态 新建状态(New) 新创建了一个线程对象. 就绪状态(Runnable) 线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中,变得可运行,等待获 ...

  3. 面试必备,Java线程状态之细节回顾

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 来源:https://dwz.cn/vYqjShos Java线程有6种状态 在某个给定时间点 ...

  4. java 线程状态_Java线程为何没有Running状态?我猜你不知道。

    作者:国栋原文:https://my.oschina.net/goldenshaw/blog/705397 Java虚拟机层面所暴露给我们的状态,与操作系统底层的线程状态是两个不同层面的事.具体而言, ...

  5. java线程切换 notify_浅谈 Java线程状态转换及控制

    作者:城北有个混子 出自:博客园 1.线程的状态(系统层面) 一个线程被创建后就进入了线程的生命周期.在线程的生命周期中,共包括新建(New).就绪(Runnable).运行(Running).阻塞( ...

  6. java 线程状态_面试官问:为什么Java线程没有Running状态?我懵了

    点击上方"占小狼的博客",选择"设为星标" 本文阅读时间大约4分钟. 来源:https://dwz.cn/dLRLBZab Java虚拟机层面所暴露给我们的状态 ...

  7. 【图解】透彻Java线程状态转换

    大家好,我是阿星,好久不见,欢迎来到Java并发编程系列番外篇线程状态转换,内容通俗易懂,请放心食用. 线程状态 先来个开场四连问 Java线程状态有几个? Java线程状态是如何转换? Java线程 ...

  8. java 线程状态_【19期】为什么Java线程没有Running状态?

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

  9. JAVA线程状态的10种转换

    JAVA线程状态转换 参考视频:https://www.bilibili.com/video/BV16J411h7Rd 后续将继续补充,同时完善. 情况1NEW–>RUNNABLE ​ 当调用t ...

最新文章

  1. linux打包运行python文件_Linux下安装pyinstaller用于将py文件打包生成一个可执行文件...
  2. Shell编程之变量赋值和引用
  3. java 求最大公因数_求最大公约数的三种算法(java实现)
  4. dcmtk编译 android,windows下编译dcmtk的Android版本
  5. 如何安装.net framework?Win11安装net framework的方法
  6. AndroidStudio:设计一个能在图片上涂鸦的程序
  7. Chrome谷歌浏览器登入指引
  8. 带权图上的三个问题--最短路问题、中国邮递员问题与货郎担问题
  9. 微信小程序 java nodejs校园自动点餐系统
  10. Oralce Autovue Web Server (Servlet)启动异常
  11. 微信小程序自定义组件/插件等解析
  12. Go语言和php个和lua,当Go遇上了Lua,会发生什么
  13. 在家想远程公司电脑?Python+微信一键连接!
  14. 开源资产管理软件OCS+GLPI安装配置
  15. keyshot聚光灯_KeyShot 6使用技巧
  16. 新浪邮箱(@sina.com/@sina.cn):启用IMAP4/SMTP服务+授权码
  17. magnify matlab 范围,matlabmagnify源程序(最新整理)
  18. 视频教程-SpringBoot+MongoDB+Vue前后分离-Java
  19. matlab 变压器 漏感,中频变压器漏感参数解析计算方法.doc
  20. OpenVPN点对点虚拟专网通讯简述

热门文章

  1. 【numpy】np.genfromtxt非常的慢,并且需要读取文件的10倍内存
  2. Python scapy库监听网卡,抓取HTTP包
  3. eclipse Hibernate
  4. c3074 无法使用带圆括号的_助力带分类简介
  5. java swing计算机_使用java swing仿window7标准计算器界面
  6. 7.Spring Security 退出登录
  7. 三天学会Selenium,阿里p7精心整理自动化测试Selenium大礼包
  8. 8年面试官问到:数据库自增 ID 用完了会咋样?
  9. android 壁纸 裁剪,Android图片裁剪之自由裁剪
  10. oracle修改数据高性能,oracle数据库的性能调整