WAITING(TIMED_WAITING) 与 BLOCKED

看《Java特种兵》的时候发现,Thread.join可以使线程进入WAITING状态,再结合姊妹篇线程的状态我们可以了解到,有两个类状态非常接近:WAITING(TIMED_WAITING) 与 BLOCKED,这两者都会让线程看上去“阻塞”在某处了。

什么时候线程会进入WAITING(无限期等待)的状态中呢?常用的有两个,分别是①Object.wait without timeout,②Thread.join without timeout【另外还有③LockSupport的park方法,④Conditon的await方法】;TIMED_WAITING除了①Object.wait with timeout、②Thread.join with timeout,还需要添加一条③Thread.sleep方法【另外还有④LockSupport的parkNanos方法,带有时间】。

在进入WAITING状态前,线程会将持有的锁先释放掉。WAITING状态中的线程需要被其他线程对同一个对象调用notify()或notifyAll()方法才能唤醒。被notifyAll()唤醒后的线程,拿不到锁的线程将进入BLOCKED状态,直到它们拿到锁为止。简而言之,WAITING类状态中的线程和BLOCKED状态中的线程的区别在于:WAITING状态的线程需要被其他线程唤醒;BLOCKED中的线程,需要等待其他线程释放锁,此处的锁特指synchronized块。

见下图

Join为什么会使线程进入WAITING(TIMED_WAITING) 状态中呢?我们看一下底层代码

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
​
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
​
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从12、20行中可以看出,join方法底层调用的就是wait方法。

sleep与wait

同样会使线程进入TIMED_WAITING状态中的sleep和wait方法,它们之间有什么区别呢?

wait()

  1. wait()方法释放锁
  2. wait()是Object类的方法
  3. wait()是非静态方法

    public final void wait() throws InterruptedException { //...}
  4. wait()应该被notify()或者notifyAll()方法唤醒
  5. wait()方法需要在循环中调用(推荐不强制)
  6. waint()方法必须放在synchronized上下文(synchronized方法或代码块)中,不然会抛出IllegalMonitorStateException

sleep()

  1. sleep()方法不会释放锁
  2. sleep()是java.lang.Thread类的方法
  3. sleep()是静态方法

    public static void sleep(long millis, int nanos) throws InterruptedException { //... }
  4. sleep()方法是在特定时间后结束
  5. sleep()方法最好不在放在循环中
  6. sleep()方法可以在任意地方执行

wait为什么要放在循环中?

synchronized(obj) {
    while (!condition) { 
      obj.wait(); 
    }
}
这里引用一段《Effective Java》
始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性(liveness)是必要的。如果条件已经成立,并且在线程等待之前,notify (或者notifyAll)方法已经被调用, 则无法保证该线程将会从等待中苏醒过来。
在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性(safety)是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系。当条件不成立时,有下面一些理由可使一个线程苏醒过来:
- 另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。 
- 条件并不成立,但是另一个线程可能意外地或恶意地调用了 notify。在公有可访问的对象上等待,这些类实际上把自己暴露在了这种危险的境地中。公有可访问对象的同步方法中包含的wait都会出现这样知问题。
- 通知线程(notifying thread)在唤醒等待线程时可能会过度“大方”。例如,即使只有某一些等待线程的条件已经被满足,但是通知线程可能仍然调用notifyAll。 
- 在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为“伪唤醒 (spurious wakeup)”

我们针对【跳过等待】和【继续等待】举个形象的例子:

针对【前置判断,跳过等待】:如果是两个狙击手,在同时等待一个人(锁),判断条件是这个人还活着。如果没有前置的判断,在等待前不校验这个人是否活着,那么当狙击手甲杀死目标并通知狙击手乙之后,乙才进入等待状态,那么乙将会死等一个已经被杀死的目标,乙将失去活性(liveness)。

针对【后置判断,继续等待】:还是两个狙击手,如果他们被唤醒后,没有后置校验,那么将导致可笑的错误,比如狙击手甲已经将目标杀死了,狙击手乙被唤醒后,没有再校验条件,直接开枪杀人,将会杀死目标两次。如果是幂等的还则罢了,不幂等的将导致错误。

综上所述,wait前后都要校验,而最好的办法就是循环。

Thread.join后谁在WAITING?

从上文中我们已经知道了,join可以使线程处于WAITING状态,那问题来了,是子线程处于WAITING状态还是父线程处于WAITING状态?我们做个小试验:

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().setName("TestJoin main....");
        Thread joinThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
​
                }
            }
        }, "join thread");
​
        joinThread.start();
        joinThread.join();
    }
}

按照在线程的状态中提供的方法,我们可以得到:

子线程即join的线程依旧是RUNNABLE状态

"join thread" #10 prio=5 os_prio=31 tid=0x00007fca1b801000 nid=0x5503 runnable [0x0000700001725000]

java.lang.Thread.State: RUNNABLE

    at com.meituan.java8.thread.TestJoin$1.run(TestJoin.java:13)

    at java.lang.Thread.run(Thread.java:748)

Locked ownable synchronizers:

    - None

父线程(在此例中是主线程)为WAITING状态

"TestJoin main...." #1 prio=5 os_prio=31 tid=0x00007fca1c003000 nid=0x1903 in Object.wait() [0x00007000003ec000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076ad94fa0> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1252)
        - locked <0x000000076ad94fa0> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1326)
        at com.meituan.java8.thread.TestJoin.main(TestJoin.java:20)
   Locked ownable synchronizers:
        - None

我们对代码做稍稍改动,可以验证sleep后的线程在什么状态

        Thread joinThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.MINUTES.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "sleeping thread");
"sleeping thread" #10 prio=5 os_prio=31 tid=0x00007f92620bc000 nid=0x5503 waiting on condition [0x00007000077b7000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:340)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at com.meituan.java8.thread.TestJoin$1.run(TestJoin.java:16)
        at java.lang.Thread.run(Thread.java:748)
   Locked ownable synchronizers:
        - None

参考文档

  1. https://javaconceptoftheday.c...

    A thread enters into WAITING state when it calls wait() or join() method on an object. Before entering into WAITING state, thread releases the lock of the object it holds. It will remain in WAITING state until any other thread calls either notify() or notifyAll() on the same object.
    Once the other thread calls notify() or notifyAll() on the same object, one or all the threads which are WAITING for lock of that object will be notified. All the notified threads will not get the object lock immediately. They will get the object lock on a priority basis once the current thread releases the lock. Until that they will be in BLOCKED state.
    In simple terms, a thread will be in WAITING state if it is waiting for notification from other threads. A thread will be in BLOCKED state if it is waiting for other thread to release the lock it wants.
  2. https://stackoverflow.com/que...

    The difference is relatively simple.
    In the BLOCKED state, a thread is about to enter a synchronized block, but there is another thread currently running inside a synchronized block on the same object. The first thread must then wait for the second thread to exit its block.
    In the WAITING state, a thread is waiting for a signal from another thread. This happens typically by calling Object.wait(), or Thread.join(). The thread will then remain in this state until another thread calls Object.notify(), or dies.
  3. https://stackoverflow.com/a/3...

    wait()
    wait() method releases the lock.
    wait() is the method of Object class.
    wait() is the non-static method - public final void wait() throws InterruptedException { //...}
    wait() should be notified by notify() or notifyAll() methods.
    wait() method needs to be called from a loop in order to deal with false alarm.
    wait() method must be called from synchronized context (i.e. synchronized method or block), otherwise it will throw IllegalMonitorStateException
    sleep()
    sleep() method doesn't release the lock.
    sleep() is the method of java.lang.Thread class.
    sleep() is the static method - public static void sleep(long millis, int nanos) throws InterruptedException { //... }
    after the specified amount of time, sleep() is completed.
    sleep() better not to call from loop(i.e. see code below).
    sleep() may be called from anywhere. there is no specific requirement.

4.《Effective Java》第10章


菟潞寺沙弥
303 声望55 粉丝