一塊兒學併發編程 - 等待與通知

waitnotifynotifyAll,這些在多線程中被常常用到的保留關鍵字,在實際開發的時候不少時候卻並無被你們重視,而本文則是對這些關鍵字的使用進行描述。java

<!-- more -->git

存在即合理

在java中,每一個對象都有兩個池,鎖池(monitor)和等待池(waitset),每一個對象又都有waitnotifynotifyAll方法,使用它們能夠實現線程之間的通訊,只是平時用的較少。算法

  • wait(): 使當前線程處於等待狀態,直到另外的線程調用notifynotifyAll將它喚醒
  • notify(): 喚醒該對象監聽的其中一個線程(規則取決於JVM廠商,FILO,FIFO,隨機...)
  • notifyAll(): 喚醒該對象監聽的全部線程

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

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

注意事項

  • 在調用wait(), notify()或notifyAll()的時候,都必須得到某個對象(注意:不是類)的鎖
  • 永遠在循環(loop)裏調用 waitnotify,而不是在 If 語句中
  • 永遠在synchronized的函數或對象裏使用wait、notify和notifyAll,否則Java虛擬機會生成 IllegalMonitorStateException

使用案例 - 生產消費

private static int i = 0;

static void product() {//生產者
    System.out.println("P->" + (++i));
}

static void consumer() {//消費者
    System.out.println("C->" + i);
}

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            product();
        }
    }).start();
    new Thread(() -> {
        while (true) {
            consumer();
        }
    }).start();
}

////////////////////////日誌////////////////////////
//P->1
//P->2
//P->3
//P->4
//C->1 
////////////////////////日誌////////////////////////

分析: 從日誌中能夠看到數據會出現屢次生產或屢次消費的問題,由於在線程執行過程當中,兩個線程缺乏協做關係,都是各幹各的,T1線程只管生產數據,無論T2線程是否處理了。函數

改進方案 - 變量消息傳遞

private static int i = 0;
private static boolean isProduction = true;

static void product() {//生產者
    if (isProduction) {
        System.out.println("P->" + (++i));
        isProduction = false;
    }
}

static void consumer() {//消費者
    if (!isProduction) {
        System.out.println("C->" + i);
        isProduction = true;
    }
}

分析: 這種狀況下咱們經過維護一個變量的方式,通知對方,可是效率及其差,線程頻繁請求與判斷大大的浪費了系統資源,雖然知足了當前要求,但並不是是可選方案...oop

改進方案 - wait/notify

上文已經介紹了使用waitnotify的前提了,接下來看案例spa

private final static byte[] LOCK = new byte[0];//定義一個鎖對象
private static boolean isProduction = true;//消息投遞
private static int i = 0;//生產的消息

static void product() {
    synchronized (LOCK) {// 必須是在 synchronized中 使用 wait/notify/notifyAll
        try {
            if (isProduction) {//若是標示位爲生產狀態,則繼續生產
                System.out.println("P->" + (++i));
                isProduction = false;
                LOCK.notify();//消費者能夠消費了
            } else {
                LOCK.wait();//說明生產出來的數據還未被消費掉
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

static void consumer() {
    try {
        synchronized (LOCK) {
            if (isProduction) {//若是當前還在生產,那麼就暫停消費者線程
                LOCK.wait();
            } else {
                System.out.println("C->" + i);
                isProduction = true;
                LOCK.notify();//通知我已經消費完畢了
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            product();
        }
    }).start();
    new Thread(() -> {
        while (true) {
            consumer();
        }
    }).start();
}
////////////////////////日誌////////////////////////
//P->1
//C->1
//P->2
//C->2
//.....
//P->354217
//C->354217
////////////////////////日誌////////////////////////

分析: 一切都是那麼完美,在T1線程中,調用LOCK.wait()將當前線程移入等待池中,並交出執行權,鎖池中的其餘線程去競爭並取得鎖的使用權(T2線程獲取),當T2線程消費完畢後,調用LOCK.notify()方法通知當前對象鎖等待池中的其中一個線程(由於這裏notify是基於JVM算法而定,由於咱們只有兩個線程,因此T1線程會接收到T2線程發出的通知,從而繼續生產數據。線程

問題: 雖然一對一沒有問題,但假設多個生產者多個消費者的狀況下怎麼辦呢?日誌

BUG - 埋點

public static void main(String[] args) {
    Stream.of("P1", "P2", "P3", "P4").forEach(name -> new Thread(() -> {
        while (true) {
            product();
        }
    }, name).start());
    Stream.of("C1", "C2").forEach(name -> new Thread(() -> {
        while (true) {
            consumer();
        }
    }, name).start());
}
////////////////////////日誌////////////////////////
//P2 -> 1
//C2 -> 1
//P2 -> 2
//C1 -> 2
//P3 -> 3
////////////////////////日誌////////////////////////

分析: 竟然不執行了,藉助前面說過的 死鎖分析知識,咱們看看是否是發生死鎖了

線程等待

結果代表,雖然沒有Found one deadlock...字眼,可是能夠看到有個線程都被wait住了,沒有被釋放,因此致使咱們當前沒法繼續生產消費

解決方案 - notifyAll

LOCK.notifyAll();//通知全部線程,我已經消費完畢了,大家繼續生產

分析: 這裏只修改了一句代碼,就是將consumer方法中的notify -> notifyAll,由通知單個線程變成通知全部在等待池中的線程

P1 -> 1
C1 -> 1
P2 -> 2
C2 -> 2
...
P3 -> 42894
C1 -> 42894
...
P1 -> 42898
C1 -> 42898

- 說點什麼

全文代碼:https://gitee.com/battcn/battcn-concurent/tree/master/Chapter1-1/battcn-thread/src/main/java/com/battcn/chapter7

  • 我的QQ:1837307557
  • battcn開源羣(適合新手):391619659

微信公衆號:battcn(歡迎調戲)

喜大普奔,迎來了十一國慶節....

相關文章
相關標籤/搜索