面试官 Q:你讲下线程状态中的 WAITING 状态,什么时候会处于这个状态?什么时候来到这个状态?
小菜 J 会心一笑 …
一个正在无限期期待另一个线程执行一个特地的动作的线程处于 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 timeoutThread.join
with no timeoutLockSupport.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 状态相似: