Java多線程學習(四)等待/通知(wait/notify)機制

系列文章傳送門:java

Java多線程學習(一)Java多線程入門git

Java多線程學習(二)synchronized關鍵字(1)程序員

java多線程學習(二)synchronized關鍵字(2) github

Java多線程學習(三)volatile關鍵字面試

Java多線程學習(四)等待/通知(wait/notify)機制編程

Java多線程學習(五)線程間通訊知識點補充微信

Java多線程學習(六)Lock鎖的使用多線程

Java多線程學習(七)併發編程中一些問題併發

系列文章將被優先更新於微信公衆號<font color="red">「Java面試通關手冊」</font>,歡迎廣大Java程序員和愛好技術的人員關注。ide

本節思惟導圖:
本節思惟導圖

思惟導圖源文件+思惟導圖軟件關注微信公衆號:「Java面試通關手冊」 回覆關鍵字:「Java多線程」 免費領取。

一 等待/通知機制介紹

1.1 不使用等待/通知機制

當兩個線程之間存在<font color="red">生產和消費者關係</font>,也就是說<font color="red">第一個線程(生產者)作相應的操做而後第二個線程(消費者)感知到了變化又進行相應的操做</font>。好比像下面的whie語句同樣,假設這個value值就是第一個線程操做的結果,doSomething()是第二個線程要作的事,當知足條件value=desire後才執行doSomething()。

可是這裏有個問題就是:第二個語句不停過經過輪詢機制來檢測判斷條件是否成立。<font color="red">若是輪詢時間的間隔過小會浪費CPU資源,輪詢時間的間隔太大,就可能取不到本身想要的數據。因此這裏就須要咱們今天講到的等待/通知(wait/notify)機制來解決這兩個矛盾</font>。

while(value=desire){
        doSomething();
    }

1.2 什麼是等待/通知機制?

通俗來說:

等待/通知機制在咱們生活中比比皆是,一個形象的例子就是廚師和服務員之間就存在等待/通知機制。

  1. 廚師作完一道菜的時間是不肯定的,因此菜到服務員手中的時間是不肯定的;
  2. 服務員就須要去「等待(wait)」;
  3. 廚師把菜作完以後,按一下鈴,這裏的按鈴就是「通知(nofity)」;
  4. 服務員聽到鈴聲以後就知道菜作好了,他能夠去端菜了。

用專業術語講:

等待/通知機制,是指一個線程A調用了對象O的<font color="red">wait()方法</font>進入<font color="red">等待狀態</font>,而另外一個線程B調用了對象O的<font color="red">notify()/notifyAll()方法</font>,線程A收到通知後退出<font color="red">等待隊列</font>,進入可運行狀態,進而執行後續操做。上訴兩個線程經過對象O來完成交互,而對象上的wait()方法notify()/notifyAll()方法的關係就如同開關信號同樣,用來完成等待方和通知方之間的交互工做。

1.3 等待/通知機制的相關方法

方法名稱 描述
notify() 隨機喚醒等待隊列中等待同一共享資源的 「一個線程」,並使該線程退出等待隊列,進入可運行狀態,也就是notify()方法僅通知「一個線程」
notifyAll() 使全部正在等待隊列中等待同一共享資源的 「所有線程」 退出等待隊列,進入可運行狀態。此時,優先級最高的那個線程最早執行,但也有多是隨機執行,這取決於JVM虛擬機的實現
wait() 使調用該方法的線程釋放共享資源鎖,而後從運行狀態退出,進入等待隊列,直到被再次喚醒
wait(long) 超時等待一段時間,這裏的參數時間是毫秒,也就是等待長達n毫秒,若是沒有通知就超時返回
wait(long,int) 對於超時時間更細力度的控制,能夠達到納秒

二 等待/通知機制的實現

2.1 個人第一個等待/通知機制程序

<font size="2">MyList.java</font>

public class MyList {
    private static List<String> list = new ArrayList<String>();

    public static void add() {
        list.add("anyString");
    }

    public static int size() {
        return list.size();
    }

}

<font size="2">ThreadA.java</font>

public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                if (MyList.size() != 5) {
                    System.out.println("wait begin "
                            + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("wait end  "
                            + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

<font size="2">ThreadB.java</font>

public class ThreadB extends Thread {
    private Object lock;

    public ThreadB(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    MyList.add();
                    if (MyList.size() == 5) {
                        lock.notify();
                        System.out.println("已發出通知!");
                    }
                    System.out.println("添加了" + (i + 1) + "個元素!");
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

<font size="2">Run.java</font>

public class Run {

    public static void main(String[] args) {

        try {
            Object lock = new Object();

            ThreadA a = new ThreadA(lock);
            a.start();

            Thread.sleep(50);

            ThreadB b = new ThreadB(lock);
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

<font size="2">運行結果:</font>
運行結果
從運行結果:"wait end 1521967322359"最後輸出能夠看出,<font color="red">notify()執行後並不會當即釋放鎖。</font>下面咱們會補充介紹這個知識點。

<font color="red">synchronized關鍵字</font>能夠將任何一個Object對象做爲同步對象來看待,而<font color="red">Java爲每一個Object都實現了等待/通知(wait/notify)機制的相關方法</font>,它們必須用在synchronized關鍵字同步的Object的臨界區內。經過調用<font color="red">wait()方法</font>可使處於臨界區內的線程進入<font color="red">等待狀態</font>,同時<font color="red">釋放被同步對象的鎖</font>。而<font color="red">notify()方法</font>能夠喚醒一個因調用wait操做而處於阻塞狀態中的線程,使其進入就緒狀態。被從新喚醒的線程會視圖從新得到臨界區的控制權也就是</font>鎖</font>,並繼續執行wait方法以後的代碼。若是發出notify操做時沒有處於阻塞狀態中的線程,那麼該命令會被忽略。

若是咱們這裏不經過<font color="red">等待/通知(wait/notify)機制</font>實現,而是使用以下的<font color="red">while循環</font>實現的話,咱們上面也講過會有很大的弊端。

while(MyList.size() == 5){
        doSomething();
    }

2.2線程的基本狀態

上面幾章的學習中咱們已經掌握了與線程有關的大部分API,這些API能夠改變線程對象的狀態。以下圖所示:
線程的基本狀態切換圖

  1. 新建(new):新建立了一個線程對象。
  2. 可運行(runnable):線程對象建立後,其餘線程(好比main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲 取cpu的使用權。
  3. 運行(running):可運行狀態(runnable)的線程得到了cpu時間片(timeslice),執行程序代碼。
  4. 阻塞(block):阻塞狀態是指線程由於某種緣由放棄了cpu使用權,也即讓出了cpu timeslice,暫時中止運行。直到線程進入可運行(runnable)狀態,纔有 機會再次得到cpu timeslice轉到運行(running)狀態。阻塞的狀況分三種:

    (一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放 入等待隊列(waitting queue)中。

    (二). 同步阻塞:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖 被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。

    (三). 其餘阻塞: 運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入可運行(runnable)狀態。

  5. 死亡(dead):線程run()、main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。

備註:
能夠用早起坐地鐵來比喻這個過程:

還沒起牀:sleeping

起牀收拾好了,隨時能夠坐地鐵出發:Runnable

等地鐵來:Waiting

地鐵來了,但要排隊上地鐵:I/O阻塞

上了地鐵,發現暫時沒座位:synchronized阻塞

地鐵上找到座位:Running

到達目的地:Dead

2.3 notify()鎖不釋放

<font color="red">當方法wait()被執行後,鎖自動被釋放,但執行玩notify()方法後,鎖不會自動釋放。必須執行完otify()方法所在的synchronized代碼塊後才釋放。</font>

下面咱們經過代碼驗證一下:

(完整代碼:https://github.com/Snailclimb/threadDemo/tree/master/src/wait_notifyHoldLock

<font size="2">帶wait方法的synchronized代碼塊</font>

synchronized (lock) {
                System.out.println("begin wait() ThreadName="
                        + Thread.currentThread().getName());
                lock.wait();
                System.out.println("  end wait() ThreadName="
                        + Thread.currentThread().getName());
            }

<font size="2">帶notify方法的synchronized代碼塊</font>

synchronized (lock) {
                System.out.println("begin notify() ThreadName="
                        + Thread.currentThread().getName() + " time="
                        + System.currentTimeMillis());
                lock.notify();
                Thread.sleep(5000);
                System.out.println("  end notify() ThreadName="
                        + Thread.currentThread().getName() + " time="
                        + System.currentTimeMillis());
            }

若是有三個同一個對象實例的線程a,b,c,a線程執行帶wait方法的synchronized代碼塊而後bb線程執行帶notify方法的synchronized代碼塊緊接着c執行帶notify方法的synchronized代碼塊。

<font size="2">運行效果以下:</font>
運行效果
<font color="red">這也驗證了咱們剛開始的結論:必須執行完notify()方法所在的synchronized代碼塊後才釋放。</font>

2.4 當interrupt方法遇到wait方法

<font color="red">當線程呈wait狀態時,對線程對象調用interrupt方法會出現InterrupedException異常。</font>

<font size="2">Service.java</font>

public class Service {
    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("begin wait()");
                lock.wait();
                System.out.println("  end wait()");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("出現異常了,由於呈wait狀態的線程被interrupt了!");
        }
    }
}

<font size="2">ThreadA.java</font>

public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        Service service = new Service();
        service.testMethod(lock);
    }

}

<font size="2">Test.java</font>

public class Test {

    public static void main(String[] args) {

        try {
            Object lock = new Object();

            ThreadA a = new ThreadA(lock);
            a.start();

            Thread.sleep(5000);

            a.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

<font size="2">運行結果:</font>
運行結果

參考:

《Java多線程編程核心技術》

《Java併發編程的藝術》

若是你以爲博主的文章不錯,歡迎轉發點贊。你能從中學到知識就是我最大的幸運。

歡迎關注個人微信公衆號:「Java面試通關手冊」(分享各類Java學習資源,面試題,以及企業級Java實戰項目回覆關鍵字免費領取)。另外我建立了一個Java學習交流羣(羣號:174594747),歡迎你們加入一塊兒學習,這裏更有面試,學習視頻等資源的分享。

相關文章
相關標籤/搜索