面試官:對併發熟悉嗎?談談線程間的協作(wait

一、線程的狀態

Java中線程中狀態可分為五種:New(新建狀態),Runnable(就緒狀態),Running(運行狀態),Blocked(阻塞狀態),Dead(死亡狀態)。

  • New:新建狀態,當線程創建完成時為新建狀態,即new Thread(…),還沒有調用start方法時,線程處於新建狀態。
  • Runnable:就緒狀態,當調用線程的的start方法後,線程進入就緒狀態,等待CPU資源。處於就緒狀態的線程由Java運行時系統的線程調度程序(thread scheduler)來調度。
  • Running:運行狀態,就緒狀態的線程獲取到CPU執行權以後進入運行狀態,開始執行run方法。
  • Blocked:阻塞狀態,線程沒有執行完,由於某種原因(如,I/O操作等)讓出CPU執行權,自身進入阻塞狀態。
  • Dead:死亡狀態,線程執行完成或者執行過程中出現異常,線程就會進入死亡狀態。

這五種狀態之間的轉換關係如下圖所示:

面試官:對併發熟悉嗎?談談線程間的協作(wait/notify/sleep...)

有了對這五種狀態的基本瞭解,現在我們來看看Java中是如何實現這幾種狀態的轉換的。 

二、wait/notify/notifyAll方法的使用

1、wait方法:

面試官:對併發熟悉嗎?談談線程間的協作(wait/notify/sleep...)

JDK中一共提供了這三個版本的方法,

  • wait()方法的作用是將當前運行的線程掛起(即讓其進入阻塞狀態),直到notify或notifyAll方法來喚醒線程.
  • wait(long timeout),該方法與wait()方法類似,唯一的區別就是在指定時間內,如果沒有notify或notifAll方法的喚醒,也會自動喚醒。
  • 至於wait(long timeout,long nanos),本意在於更精確的控制調度時間,不過從目前版本來看,該方法貌似沒有完整的實現該功能,其源碼(JDK1.8)如下:
<code>public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout             throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos  999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
            timeout++;
        }

        wait(timeout);
    }/<code>

從源碼來看,JDK8中對納秒的處理,只做了四捨五入,所以還是按照毫秒來處理的,可能在未來的某個時間點會用到納秒級別的精度。雖然JDK提供了這三個版本,其實最後都是調用wait(long timeout)方法來實現的,wait()方法與wait(0)等效,而wait(long timeout,int nanos)從上面的源碼可以看到也是通過wait(long timeout)來完成的。

下面我們通過一個簡單的例子來演示wait()方法的使用:

<code>package com.paddx.test.concurrent;

public class WaitTest {

    public void testWait(){
        System.out.println("Start-----");
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End-------");
    }

    public static void main(String[] args) {
        final WaitTest test = new WaitTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.testWait();
            }
        }).start();
    }
}/<code>

這段代碼的意圖很簡單,就是程序執行以後,讓其暫停一秒,然後再執行。運行上述代碼,查看結果:

<code>Start-----
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at com.paddx.test.concurrent.WaitTest.testWait(WaitTest.java:8)
    at com.paddx.test.concurrent.WaitTest$1.run(WaitTest.java:20)
    at java.lang.Thread.run(Thread.java:745)/<code>

這段程序並沒有按我們的預期輸出相應結果,而是拋出了一個異常。大家可能會覺得奇怪為什麼會拋出異常?而拋出的IllegalMonitorStateException異常又是什麼?我們可以看一下JDK中對IllegalMonitorStateException的描述:

Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.

這句話的意思大概就是:線程試圖等待對象的監視器或者試圖通知其他正在等待對象監視器的線程,但本身沒有對應的監視器的所有權。

其實這個問題在《【68期】面試官:對併發熟悉嗎?說說Synchronized及實現原理》一文中有提到過,wait方法是一個本地方法,其底層是通過一個叫做監視器鎖的對象來完成的。所以上面之所以會拋出異常,是因為在調用wait方式時沒有獲取到monitor對象的所有權,那如何獲取monitor對象所有權?

Java中只能通過Synchronized關鍵字來完成,修改上述代碼,增加Synchronized關鍵字:

<code>package com.paddx.test.concurrent;

public class WaitTest {

    public synchronized void testWait(){//增加Synchronized關鍵字
        System.out.println("Start-----");
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End-------");
    }

    public static void main(String[] args) {
        final WaitTest test = new WaitTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.testWait();
            }

        }).start();
    }
}/<code>

現在再運行上述代碼,就能看到預期的效果了:

<code>Start-----
End-------/<code>

所以,通過這個例子,大家應該很清楚,wait方法的使用必須在同步的範圍內,否則就會拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當前線程等待notify/notifyAll方法的喚醒,或等待超時後自動喚醒。

2、notify/notifyAll方法

面試官:對併發熟悉嗎?談談線程間的協作(wait/notify/sleep...)

有了對wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過對象的monitor對象來實現的,所以只要在同一對象上去調用notify/notifyAll方法,就可以喚醒對應對象monitor上等待的線程了。

notify和notifyAll的區別在於前者只能喚醒monitor上的一個線程,對其他線程沒有影響,而notifyAll則喚醒所有的線程,看下面的例子很容易理解這兩者的差別:

<code>package com.paddx.test.concurrent;

public class NotifyTest {
    public synchronized void testWait(){
        System.out.println(Thread.currentThread().getName() +" Start-----");
        try {
            wait(0);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +" End-------");
    }

    public static void main(String[] args) throws InterruptedException {
        final NotifyTest test = new NotifyTest();
        for(int i=0;i<5;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test.testWait();
                }
            }).start();
        }

        synchronized (test) {
            test.notify();
        }
        Thread.sleep(3000);
        System.out.println("-----------分割線-------------");

        synchronized (test) {

            test.notifyAll();
        }
    }
}/<code>

輸出結果如下:

<code>Thread-0 Start-----
Thread-1 Start-----
Thread-2 Start-----
Thread-3 Start-----
Thread-4 Start-----
Thread-0 End-------
-----------分割線-------------
Thread-4 End-------
Thread-3 End-------
Thread-2 End-------
Thread-1 End-------/<code>

從結果可以看出:調用notify方法時只有線程Thread-0被喚醒,但是調用notifyAll時,所有的線程都被喚醒了。

最後,有兩點需要注意:

1.調用wait方法後,線程是會釋放對monitor對象的所有權的。

2.一個通過wait方法阻塞的線程,必須同時滿足以下兩個條件才能被真正執行:

  • 線程需要被喚醒(超時喚醒或調用notify/notifyll)。
  • 線程喚醒後需要競爭到鎖(monitor)。

三、sleep/yield/join方法解析

上面我們已經清楚了wait和notify方法的使用和原理,現在我們再來看另外一組線程間協作的方法。這組方法跟上面方法的最明顯區別是:這幾個方法都位於Thread類中,而上面三個方法都位於Object類中。至於為什麼,大家可以先思考一下。現在我們逐個分析sleep/yield/join方法:

1、sleep

sleep方法的作用是讓當前線程暫停指定的時間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區別。最簡單的區別是,wait方法依賴於同步,而sleep方法可以直接調用。而更深層次的區別在於sleep方法只是暫時讓出CPU的執行權,並不釋放鎖。而wait方法則需要釋放鎖。

<code>package com.paddx.test.concurrent;

public class SleepTest {
    public synchronized void sleepMethod(){
        System.out.println("Sleep start-----");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Sleep end-----");
    }

    public synchronized void waitMethod(){
        System.out.println("Wait start-----");
        synchronized (this){
            try {
                wait(1000);
            } catch (InterruptedException e) {

                e.printStackTrace();
            }
        }
        System.out.println("Wait end-----");
    }

    public static void main(String[] args) {
        final SleepTest test1 = new SleepTest();

        for(int i = 0;i<3;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test1.sleepMethod();
                }
            }).start();
        }


        try {
            Thread.sleep(10000);//暫停十秒,等上面程序執行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----分割線-----");

        final SleepTest test2 = new SleepTest();

        for(int i = 0;i<3;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test2.waitMethod();
                }
            }).start();
        }

    }
}/<code>

執行結果:

<code>Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----

-----分割線-----
Wait start-----
Wait start-----
Wait start-----
Wait end-----
Wait end-----
Wait end-----/<code>

這個結果的區別很明顯,通過sleep方法實現的暫停,程序是順序進入同步塊的,只有當上一個線程執行完成的時候,下一個線程才能進入同步方法,sleep暫停期間一直持有monitor對象鎖,其他線程是不能進入的。而wait方法則不同,當調用wait方法後,當前線程會釋放持有的monitor對象鎖,因此,其他線程還可以進入到同步方法,線程被喚醒後,需要競爭鎖,獲取到鎖之後再繼續執行。

2、yield方法

yield方法的作用是暫停當前線程,以便其他線程有機會執行,不過不能指定暫停的時間,並且也不能保證當前線程馬上停止。yield方法只是將Running狀態轉變為Runnable狀態。我們還是通過一個例子來演示其使用:

<code>package com.paddx.test.concurrent;

public class YieldTest implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName() + ": " + i);

            Thread.yield();
        }
    }

    public static void main(String[] args) {
        YieldTest runn = new YieldTest();
        Thread t1 = new Thread(runn,"FirstThread");
        Thread t2 = new Thread(runn,"SecondThread");

        t1.start();
        t2.start();

    }
}/<code>

運行結果如下:

<code>FirstThread: 0
SecondThread: 0
FirstThread: 1
SecondThread: 1
FirstThread: 2
SecondThread: 2
FirstThread: 3
SecondThread: 3
FirstThread: 4
SecondThread: 4/<code>

這個例子就是通過yield方法來實現兩個線程的交替執行。不過請注意:這種交替並不一定能得到保證,源碼中也對這個問題進行說明:

<code>/**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * 

 Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * 

 It is rarely appropriate to use this method. It may be useful


     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
*/

/<code>

這段話主要說明了三個問題:

  • 調度器可能會忽略該方法。
  • 使用的時候要仔細分析和測試,確保能達到預期的效果。
  • 很少有場景要用到該方法,主要使用的地方是調試和測試。  

3、join方法

面試官:對併發熟悉嗎?談談線程間的協作(wait/notify/sleep...)

join方法的作用是父線程等待子線程執行完成後再執行,換句話說就是將異步執行的線程合併為同步的線程。JDK中提供三個版本的join方法,其實現與wait方法類似,join()方法實際上執行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實現方式一致,暫時對納秒的支持也是不完整的。我們可以看下join方法的源碼,這樣更容易理解:

<code>public final void join() throws InterruptedException {
        join(0);
    }

 public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis             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;
            }
        }
    }

public final synchronized void join(long millis, int nanos)
    throws InterruptedException {

        if (millis             throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos  999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        join(millis);
    }/<code>

大家重點關注一下join(long millis)方法的實現,可以看出join方法就是通過wait方法來將線程的阻塞,如果join的線程還在執行,則將當前線程阻塞起來,直到join的線程執行完成,當前線程才能執行。

不過有一點需要注意,這裡的join只調用了wait方法,卻沒有對應的notify方法,原因是Thread的start方法中做了相應的處理,所以當join的線程執行完成以後,會自動喚醒主線程繼續往下執行。下面我們通過一個例子來演示join方法的作用:

(1)不使用join方法:

<code>package com.paddx.test.concurrent;

public class JoinTest implements Runnable{
    @Override
    public void run() {

        try {
            System.out.println(Thread.currentThread().getName() + " start-----");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " end------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        for (int i=0;i<5;i++) {
            Thread test = new Thread(new JoinTest());
            test.start();
        }

        System.out.println("Finished~~~");
    }
}/<code>

執行結果如下:

<code>Thread-0 start-----
Thread-1 start-----
Thread-2 start-----
Thread-3 start-----
Finished~~~
Thread-4 start-----
Thread-2 end------
Thread-4 end------
Thread-1 end------
Thread-0 end------
Thread-3 end------/<code>

(2)使用join方法:

<code>package com.paddx.test.concurrent;

public class JoinTest implements Runnable{
    @Override
    public void run() {

        try {
            System.out.println(Thread.currentThread().getName() + " start-----");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " end------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        for (int i=0;i<5;i++) {
            Thread test = new Thread(new JoinTest());
            test.start();

            try {
                test.join(); //調用join方法
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Finished~~~");
    }
}/<code>

執行結果如下:

<code>Thread-0 start-----
Thread-0 end------
Thread-1 start-----
Thread-1 end------
Thread-2 start-----
Thread-2 end------
Thread-3 start-----
Thread-3 end------
Thread-4 start-----
Thread-4 end------
Finished~~~/<code>

對比兩段代碼的執行結果很容易發現,在沒有使用join方法之間,線程是併發執行的,而使用join方法後,所有線程是順序執行的。

四、總結

本文主要詳細講解了wait/notify/notifyAll和sleep/yield/join方法。最後回答一下上面提出的問題:wait/notify/notifyAll方法的作用是實現線程間的協作,那為什麼這三個方法不是位於Thread類中,而是位於Object類中?位於Object中,也就相當於所有類都包含這三個方法(因為Java中所有的類都繼承自Object類)。

要回答這個問題,還是得回過來看wait方法的實現原理,大家需要明白的是,wait等待的到底是什麼東西?如果對上面內容理解的比較好的話,我相信大家應該很容易知道wait等待其實是對象monitor,由於Java中的每一個對象都有一個內置的monitor對象,自然所有的類都理應有wait/notify方法。


分享到:


相關文章: