在上一篇章中咱们谈论了 WAITING 状态,在这一篇章里,咱们来看剩余的最后的一个状态:TIMED_WAITING(限时等待)。java
一个正在限时等待另外一个线程执行一个动做的线程处于这一状态。缓存
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):app
带指定的等待时间的等待线程所处的状态。一个线程处于这一状态是由于用一个指定的正的等待时间(为参数)调用了如下方法中的其一:ide
对应的英文原文以下:oop
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:ui
Thread.sleep
Object.wait
with timeoutThread.join
with timeoutLockSupport.parkNanos
LockSupport.parkUntil
不难看出,TIMED_WAITING 与 WAITING 间的联系仍是很紧密的,主要差别在时限(timeout)参数上。this
另外则是 sleep 这一点上的不一样。spa
实际上,在上一篇章中谈到的没有参数的 wait() 等价于 wait(0),而 wait(0) 它不是等0毫秒,偏偏相反,它的意思是永久的等下去,到天荒地老,除非收到通知。.net
具体可见 java 的源代码及相应 javadoc,注意:同时又还存在一种特殊的状况,所谓的“spurious wakeup”(虚假唤醒),咱们在下面再讨论。
便是把本身再次活动的命运彻底交给了别人(通知者),那么这样会存在什么问题呢?
在这里,咱们仍是继续上一篇章中的谈到的车箱场景,如不清楚的参见 Java 线程状态之 WAITING。
设想一种状况,乘务员线程增长了厕纸,正当它准备执行 notify 时,这个线程因某种缘由被杀死了(持有的锁也随之释放)。这种状况下,条件已经知足了,但等待的线程却没有收到通知,还在傻乎乎地等待。
简而言之,就是存在通知失效的状况。这时,若是有个心机婊线程,她考虑得比较周全,她不是调用 wait(),而是调用 wait(1000),若是把进入 wait set 比喻成在里面睡觉等待。那么 wait(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(); // 一直等待的线程1 Thread 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毫秒的线程2 Thread 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_WAITING assertThat(passenger1.getState()).isEqualTo(Thread.State.WAITING); assertThat(passenger2.getState()).isEqualTo(Thread.State.TIMED_WAITING); // 此时的纸张数应为0 assertThat(toilet.paperCount).isEqualTo(0); // 乘务员线程启动 steward.start(); // 确保已经增长纸张 Thread.sleep(100); // 此时的纸张数应为10 assertThat(toilet.paperCount).isEqualTo(10); // 确保线程2已经自我唤醒 Thread.sleep(1000); // 若是纸张已经被消耗一张,说明线程2已经成功自我唤醒 assertThat(toilet.paperCount).isEqualTo(9); }
虽然,前面说到没有参数的 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,两个消费者同时起来,其中一个先抢到了锁,进行了消费,等另外一个也抢到锁时,可能条件又不知足了,因此仍是要继续判断,不能简单认为被唤醒了就是条件知足了。
关于此话题的更多信息,可参考:
进入 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 和 TIMED_WAITING 后,咱们能够综合来看看它们,好比,阻塞与等待到底有什么本质的区别呢?
显然,BLOCKED 一样能够视做是一种特殊的,隐式的 wait/nofity 机制。等待的条件就是“有锁仍是没锁”。
不过,这是一个不肯定的等待,可能等待(没法获取锁时),也可能不等待(能获取锁)。陷入这种阻塞后也没有自主退出的机制。
有一点须要注意的是,BLOCKED 状态是与 Java 语言级别的 synchronized 机制相关的,咱们知道在 Java 5.0 以后引入了更多的机制(java.util.concurrent),除了能够用 synchronized 这种内部锁,也可使用外部的显式锁。
显式锁有一些更好的特性,如能中断,能设置获取锁的超时,可以有多个条件等,尽管从表面上说,当显式锁没法获取时,咱们仍是说,线程被“阻塞”了,但却未必是 BLOCKED 状态。
当锁可用时,其中的一个线程会被系统隐式通知,并被赋予锁,从而得到在同步块中的执行权。
显然,等待锁的线程与系统同步机制造成了一个协做关系。
对比来看, WAITING 状态属于主动地显式地申请的阻塞,BLOCKED 则属于被动的阻塞,但不管从字面意义仍是从根本上来看,并没有本质的区别。
在前面咱们也已经说过,这三个状态能够认为是传统 waiting 状态在 JVM 层面的一个细分。
最后,跟传统进(线)程状态划分的一个最终对比:
关于 Java 线程状态的全部分析就到此为止。