java 併發——理解 wait / notify / notifyAll

1、前言

前情簡介:
java 併發——內置鎖
java 併發——線程html

java 面試是否有被問到過,sleepwait 方法的區別,關於這個問題其實不用多說,大多數人都能回答出最主要的兩點區別:java

  • sleep 是線程的方法, wait / notify / notifyAll 是 Object 類的方法;
  • sleep 不會釋放當前線程持有的鎖,到時間後程序會繼續執行,wait 會釋放線程持有的鎖並掛起,直到經過 notify 或者 notifyAll 從新得到鎖。
    另外還有一些參數、異常等區別,不細說了。本文重點記錄一下 wait / notify / notifyAll 的相關知識。

2、常見的同步場景

開發中經常遇到這樣的場景:面試

一個線程執行過程當中,須要開啓另一個子線程去作某個耗時的操做(經過休眠3秒模擬),
而且**等待**子線程返回結果,主線程再根據返回的結果繼續往下執行。

這裏注意我上面加*兩個字「等待」。若是不須要等待,單純只是對子線程的結果作處理,咱們大可註冊回調方法解決問題,此文再也不贅述接口回調。
此處場景就是主線程停下來等待子線程執行完畢後,主線程再繼續執行。針對該場景下面給出實現:編程

設置一個判斷的標誌位

volatile boolean flag = false;

    public void test(){
        //...

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("--- 休眠 3 秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                flag = true;
            }
        });
        t1.start();

        while(!flag){

        }
        System.out.println("--- work thread run");
    }

上面的代碼,執行結果:
緩存

強調一點,聲明標誌位的時候,必定注意 volatile 關鍵字不能忘,若是不加該關鍵字修飾,程序可能進入死循環。這是同步中的可見性問題,在 《java 併發——內置鎖》 中有記錄。
顯然,這個實現方案並很差,原本主線程什麼也不用作,卻一直在競爭資源,作空循環,性能上很差,因此並不推薦。併發

線程的 join 方法

public void test(){
        //...

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("--- 休眠 3 秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("--- work thread continue");
    }

上面的代碼,執行結果同上。利用 Thread 類的 join 方法實現了同步,達到了效果,可是 join 方法不能必定保證效果,在不一樣的 cpu 上,可能呈現出意想不到的結果,因此儘可能不要用上述方法。dom

使用閉鎖 CountDownLatch

不清楚閉鎖的新同窗可點擊文章開頭給出的另外一篇文章,《java 併發——線程》。ide

public void test(){
        //...

        final CountDownLatch countDownLatch = new CountDownLatch(1);

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("--- 休眠 3 秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        });

        t1.start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("--- work thread run");
    }

上面的代碼,執行結果同上。一樣能夠實現上述效果,執行結果和上面同樣。該方法推薦使用。oop

利用 wait / notify 優化標誌位方法

爲了方便對比,首先給 2.1 中的循環方法增長一些打印。修改後的代碼以下:性能

volatile boolean flag = false;

    public void test() {
        //...
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("--- 休眠 3 秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                flag = true;
            }
        });
        t1.start();

        while (!flag) {
            try {
                System.out.println("---while-loop---");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("--- work thread run");
    }

執行結果以下:

事實證實,while 循環確實一直在執行。

爲了使該線程再不須要執行的時候不搶佔資源,咱們能夠利用 wait 方法將其掛起,在須要它執行的時候,再利用 notify 方法將其喚醒。這樣達到優化的目的,優化後的代碼以下:

volatile boolean flag = false;

    public void test() {
        //...
        final Object obj = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                try {
                    Thread.sleep(3000);
                    System.out.println("--- 休眠 3 秒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    flag = true;
                }
                obj.notify();
            }
        });
        t1.start();

        synchronized (obj) {
            while (!flag) {
                try {
                    System.out.println("---while-loop---");
                    Thread.sleep(500);
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("--- work thread run");

    }

執行結果:

結果證實,優化後的程序,循環只執行了一次。

3、理解 wait / notify / notifyAll

在Java中,每一個對象都有兩個池,鎖(monitor)池和等待池

鎖池

鎖池:假設線程A已經擁有了某個對象的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),因爲這些線程在進入對象的synchronized方法以前必須先得到該對象的鎖的擁有權,可是該對象的鎖目前正被線程A擁有,因此這些線程就進入了該對象的鎖池中。

等待池

等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(由於wait()方法必須出如今synchronized中,這樣天然在執行wait()方法以前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。若是另外的一個線程調用了相同對象的notifyAll()方法,那麼處於該對象的等待池中的線程就會所有進入該對象的鎖池中,準備爭奪鎖的擁有權。若是另外的一個線程調用了相同對象的notify()方法,那麼僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池.

notify 和 notifyAll 的區別

wait()

public final void wait() throws InterruptedException,IllegalMonitorStateException
該方法用來將當前線程置入休眠狀態,直到接到通知或被中斷爲止。在調用 wait()以前,線程必需要得到該對象的對象級別鎖,即只能在同步方法或同步塊中調用 wait()方法。進入 wait()方法後,當前線程釋放鎖。在從 wait()返回前,線程與其餘線程競爭從新得到鎖。若是調用 wait()時,沒有持有適當的鎖,則拋出 IllegalMonitorStateException,它是 RuntimeException 的一個子類,所以,不須要 try-catch 結

notify()

public final native void notify() throws IllegalMonitorStateException
該方法也要在同步方法或同步塊中調用,即在調用前,線程也必需要得到該對象的對象級別鎖,的若是調用 notify()時沒有持有適當的鎖,也會拋出 IllegalMonitorStateException。
該方法用來通知那些可能等待該對象的對象鎖的其餘線程。若是有多個線程等待,則線程規劃器任意挑選出其中一個 wait()狀態的線程來發出通知,並使它等待獲取該對象的對象鎖(notify 後,當前線程不會立刻釋放該對象鎖,wait 所在的線程並不能立刻獲取該對象鎖,要等到程序退出 synchronized 代碼塊後,當前線程纔會釋放鎖,wait所在的線程也才能夠獲取該對象鎖),但不驚動其餘一樣在等待被該對象notify的線程們。當第一個得到了該對象鎖的 wait 線程運行完畢之後,它會釋放掉該對象鎖,此時若是該對象沒有再次使用 notify 語句,則即使該對象已經空閒,其餘 wait 狀態等待的線程因爲沒有獲得該對象的通知,會繼續阻塞在 wait 狀態,直到這個對象發出一個 notify 或 notifyAll。這裏須要注意:它們等待的是被 notify 或 notifyAll,而不是鎖。這與下面的 notifyAll()方法執行後的狀況不一樣。

notifyAll()

public final native void notifyAll() throws IllegalMonitorStateException
該方法與 notify ()方法的工做方式相同,重要的一點差別是:
notifyAll 使全部原來在該對象上 wait 的線程通通退出 wait 的狀態(即所有被喚醒,再也不等待 notify 或 notifyAll,但因爲此時尚未獲取到該對象鎖,所以還不能繼續往下執行),變成等待獲取該對象上的鎖,一旦該對象鎖被釋放(notifyAll 線程退出調用了 notifyAll 的 synchronized 代碼塊的時候),他們就會去競爭。若是其中一個線程得到了該對象鎖,它就會繼續往下執行,在它退出 synchronized 代碼塊,釋放鎖後,其餘的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到全部被喚醒的線程都執行完畢。

4、生產者與消費者模式

生產者與消費者問題是併發編程裏面的經典問題。接下來講說利用wait()和notify()來實現生產者和消費者併發問題:
顯然要保證生產者和消費者併發運行不出亂,主要要解決:當生產者線程的緩存區爲滿的時候,就應該調用wait()來中止生產者繼續生產,而當生產者滿的緩衝區被消費者消費掉一塊時,則應該調用notify()喚醒生產者,通知他能夠繼續生產;一樣,對於消費者,當消費者線程的緩存區爲空的時候,就應該調用wait()停掉消費者線程繼續消費,而當生產者又生產了一個時就應該調用notify()來喚醒消費者線程通知他能夠繼續消費了。
下面是一個簡單的代碼實現:

package com.sharpcj;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        Reposity reposity = new Reposity(600);
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for(int i = 0; i < 10; i++){
            threadPool.submit(new Producer(reposity));
        }

        for(int i = 0; i < 10; i++){
            threadPool.submit(new Consumer(reposity));
        }
        threadPool.shutdown();
    }
}


class Reposity {
    private static final int MAX_NUM = 2000;
    private int currentNum;

    private final Object obj = new Object();

    public Reposity(int currentNum) {
        this.currentNum = currentNum;
    }

    public void in(int inNum) {
        synchronized (obj) {
            while (currentNum + inNum > MAX_NUM) {
                try {
                    System.out.println("入貨量 " + inNum + " 線程 " + Thread.currentThread().getId() + "被掛起...");
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            currentNum += inNum;
            System.out.println("線程: " + Thread.currentThread().getId() + ",入貨:inNum = [" + inNum + "], currentNum = [" + currentNum + "]");
            obj.notifyAll();
        }
    }

    public void out(int outNum) {
        synchronized (obj) {
            while (currentNum < outNum) {
                try {
                    System.out.println("出貨量 " + outNum + " 線程 " + Thread.currentThread().getId() + "被掛起...");
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            currentNum -= outNum;
            System.out.println("線程: " + Thread.currentThread().getId() + ",出貨:outNum = [" + outNum + "], currentNum = [" + currentNum + "]");
            obj.notifyAll();
        }
    }
}

class Producer implements Runnable {
    private Reposity reposity;

    public Producer(Reposity reposity) {
        this.reposity = reposity;
    }

    @Override
    public void run() {
        reposity.in(200);
    }
}

class Consumer implements Runnable {
    private Reposity reposity;

    public Consumer(Reposity reposity) {
        this.reposity = reposity;
    }

    @Override
    public void run() {
        reposity.out(200);
    }
}

執行結果:

5、寫在後面

最後作幾點總結:

  1. 調用wait方法和notify、notifyAll方法前必須得到對象鎖,也就是必須寫在synchronized(鎖對象){......}代碼塊中。

  2. 當線程調用了wait方法後就釋放了對象鎖,不然其餘線程沒法得到對象鎖。

  3. 當調用 wait() 方法後,線程必須再次得到對象鎖後才能繼續執行。

  4. 若是另外兩個線程都在 wait,則正在執行的線程調用notify方法只能喚醒一個正在wait的線程(公平競爭,由JVM決定)。

  5. 當使用notifyAll方法後,全部wait狀態的線程都會被喚醒,可是隻有一個線程能得到鎖對象,必須執行完while(condition){this.wait();}後才釋放對象鎖。其他的須要等待該得到對象鎖的線程執行完釋放對象鎖後才能繼續執行。

  6. 當某個線程調用notifyAll方法後,雖然其餘線程被喚醒了,可是該線程依然持有着對象鎖,必須等該同步代碼塊執行完(右大括號結束)後纔算正式釋放了鎖對象,另外兩個線程纔有機會執行。

  7. 第5點中說明, wait 方法的調用前的條件判斷需放在循環中,不然可能出現邏輯錯誤。另外,根據程序邏輯合理使用 wait 即 notify 方法,避免如先執行 notify ,後執行 wait 方法,線程一直掛起之類的錯誤。

相關文章
相關標籤/搜索