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

1、線程的狀態

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

  • New:新建狀態,當線程建立完成時爲新建狀態,即new Thread(…),尚未調用start方法時,線程處於新建狀態。面試

  • Runnable:就緒狀態,當調用線程的的start方法後,線程進入就緒狀態,等待CPU資源。處於就緒狀態的線程由Java運行時系統的線程調度程序(thread scheduler)來調度。併發

  • Running:運行狀態,就緒狀態的線程獲取到CPU執行權之後進入運行狀態,開始執行run方法。app

  • Blocked:阻塞狀態,線程沒有執行完,因爲某種緣由(如,I/O操做等)讓出CPU執行權,自身進入阻塞狀態。異步

  • Dead:死亡狀態,線程執行完成或者執行過程當中出現異常,線程就會進入死亡狀態。ide

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

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

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

一、wait方法:

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

  • wait()方法的做用是將當前運行的線程掛起(即讓其進入阻塞狀態),直到notify或notifyAll方法來喚醒線程.線程

  • wait(long timeout),該方法與wait()方法相似,惟一的區別就是在指定時間內,若是沒有notify或notifAll方法的喚醒,也會自動喚醒。

  • 至於wait(long timeout,long nanos),本意在於更精確的控制調度時間,不過從目前版原本看,該方法貌似沒有完整的實現該功能,其源碼(JDK1.8)以下:

     

public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
 
        if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
            timeout++;
        }
 
        wait(timeout);
    }

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

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

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();
    }
}

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

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)

這段程序並無按咱們的預期輸出相應結果,而是拋出了一個異常。你們可能會以爲奇怪爲何會拋出異常?而拋出的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關鍵字:

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();
    }
}

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

Start-----
End-------

因此,經過這個例子,你們應該很清楚,wait方法的使用必須在同步的範圍內,不然就會拋出IllegalMonitorStateException異常,wait方法的做用就是阻塞當前線程等待notify/notifyAll方法的喚醒,或等待超時後自動喚醒。

二、notify/notifyAll方法

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

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

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();
        }
    }
}

 

輸出結果以下:

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-------

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

最後,有兩點須要注意:

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

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

  • 線程須要被喚醒(超時喚醒或調用notify/notifyll)。

  • 線程喚醒後須要競爭到鎖(monitor)。

3、sleep/yield/join方法解析

上面咱們已經清楚了wait和notify方法的使用和原理,如今咱們再來看另一組線程間協做的方法。這組方法跟上面方法的最明顯區別是:這幾個方法都位於Thread類中,而上面三個方法都位於Object類中。至於爲何,你們能夠先思考一下。如今咱們逐個分析sleep/yield/join方法:

一、sleep

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

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();
        }
 
    }
}

執行結果:

Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
-----分割線-----
Wait start-----
Wait start-----
Wait start-----
Wait end-----
Wait end-----
Wait end-----
 

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

二、yield方法

yield方法的做用是暫停當前線程,以便其餘線程有機會執行,不過不能指定暫停的時間,而且也不能保證當前線程立刻中止。yield方法只是將Running狀態轉變爲Runnable狀態。咱們仍是經過一個例子來演示其使用:

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();
 
    }
}

運行結果以下:

FirstThread: 0
SecondThread: 0
FirstThread: 1
SecondThread: 1
FirstThread: 2
SecondThread: 2
FirstThread: 3
SecondThread: 3
FirstThread: 4
SecondThread: 4

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

/**
     * 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.
     *
     * <p> 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.
     *
     * <p> 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.
*/
 

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

  • 調度器可能會忽略該方法。

  • 使用的時候要仔細分析和測試,確保能達到預期的效果。

  • 不多有場景要用到該方法,主要使用的地方是調試和測試。  

三、join方法

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

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 < 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;
            }
        }
    }
 
public final synchronized void join(long millis, int nanos)
    throws InterruptedException {
 
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
 
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
 
        join(millis);
    }
 

你們重點關注一下join(long millis)方法的實現,能夠看出join方法就是經過wait方法來將線程的阻塞,若是join的線程還在執行,則將當前線程阻塞起來,直到join的線程執行完成,當前線程才能執行。

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

(1)不使用join方法:

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~~~");
    }
}

執行結果以下:

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------

(2)使用join方法:

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~~~");
    }
}

執行結果以下:

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~~~
 

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

4、總結

本文主要詳細講解了wait/notify/notifyAll和sleep/yield/join方法。最後回答一下上面提出的問題:wait/notify/notifyAll方法的做用是實現線程間的協做,那爲何這三個方法不是位於Thread類中,而是位於Object類中?位於Object中,也就至關於全部類都包含這三個方法(由於Java中全部的類都繼承自Object類)。

要回答這個問題,仍是得回過來看wait方法的實現原理,你們須要明白的是,wait等待的究竟是什麼東西?若是對上面內容理解的比較好的話,我相信你們應該很容易知道wait等待實際上是對象monitor,因爲Java中的每個對象都有一個內置的monitor對象,天然全部的類都理應有wait/notify方法。

相關文章
相關標籤/搜索