wait和notify的坑

也許我們只知道wait和notify是實現(xiàn)線程通信的吼拥,同時要使用synchronized包住倚聚,其實在開發(fā)中知道這個是遠遠不夠的。接下來看看兩個常見的問題凿可。

問題一:通知丟失

創(chuàng)建2個線程惑折,一個線程負責計算授账,一個線程負責獲取計算結果。


public class Calculator extends Thread {
    int total;

    @Override
    public void run() {
        synchronized (this){
            for(int i = 0; i < 101; i++){
                total += i;
            }
            this.notify();
        }

    }
}

public class ReaderResult extends Thread {
    Calculator c;
    public ReaderResult(Calculator c) {
        this.c = c;
    }

    @Override
    public void run() {
        synchronized (c) {
            try {
                System.out.println(Thread.currentThread() + "等待計算結...");
                c.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
        }
    }


    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        //先啟動獲取計算結果線程
        new ReaderResult(calculator).start();
        calculator.start();
        
    }
}

我們會獲得預期的結果:
Thread[Thread-1,5,main]等待計算結...
Thread[Thread-1,5,main]計算結果為:5050

但是我們修改為先啟動計算線程呢惨驶?
calculator.start();
new ReaderResult(calculator).start();

這是獲取結算結果線程一直等待:
Thread[Thread-1,5,main]等待計算結...

問題分析

打印出線程堆棧:

"Thread-1" prio=5 tid=0x00007f983b87e000 nid=0x4d03 in Object.wait() [0x0000000118988000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d56fb4d0> (a com.concurrent.waitnotify.Calculator)
    at java.lang.Object.wait(Object.java:503)
    at com.concurrent.waitnotify.ReaderResult.run(ReaderResult.java:18)
    - locked <0x00000007d56fb4d0> (a com.concurrent.waitnotify.Calculator)

可以看出ReaderResult在Calculator上等待白热。發(fā)生這個現(xiàn)象就是常說的通知丟失,在獲取通知前粗卜,通知提前到達屋确,我們先計算結果,計算完后再通知续扔,但是這個時候獲取結果沒有在等待通知攻臀,等到獲取結果的線程想獲取結果時,這個通知已經(jīng)通知過了纱昧,所以就發(fā)生丟失刨啸,那我們該如何避免?可以設置變量表示是否被通知過,修改代碼如下:

public class Calculator extends Thread {
    int total;
    boolean isSignalled = false;

    @Override
    public void run() {
        synchronized (this) {
            isSignalled = true;//已經(jīng)通知過
                for (int i = 0; i < 101; i++) {
                    total += i;
                }
                this.notify();
            }
    }
}

public class ReaderResult extends Thread {

    Calculator c;

    public ReaderResult(Calculator c) {
        this.c = c;
    }

    @Override
    public void run() {
        synchronized (c) {
            if (!c.isSignalled) {//判斷是否被通知過
                try {
                    System.out.println(Thread.currentThread() + "等待計算結...");
                    c.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "計算結果為:" + c.total);
            }

        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        new ReaderResult(calculator).start();
        calculator.start();
    }
}

問題二:假喚醒

兩個線程去刪除數(shù)組的元素砌些,當沒有元素的時候等待呜投,另一個線程添加一個元素,添加完后通知刪除數(shù)據(jù)的線程存璃。

public class EarlyNotify{
    private List list;

    public EarlyNotify() {
        list = Collections.synchronizedList(new LinkedList());
    }

    public String removeItem() throws InterruptedException {

        synchronized ( list ) {
            if ( list.isEmpty() ) {  //問題在這
                list.wait();
            }

            //刪除元素
            String item = (String) list.remove(0);
            return item;
        }
    }

    public void addItem(String item) {
        synchronized ( list ) {
            //添加元素
            list.add(item);
            //添加后仑荐,通知所有線程
            list.notifyAll();
        }
    }

    private static void print(String msg) {
        String name = Thread.currentThread().getName();
        System.out.println(name + ": " + msg);
    }

    public static void main(String[] args) {
        final EarlyNotify en = new EarlyNotify();

        Runnable runA = new Runnable() {
            public void run() {
                try {
                    String item = en.removeItem();

                } catch ( InterruptedException ix ) {
                    print("interrupted!");
                } catch ( Exception x ) {
                    print("threw an Exception!!!\n" + x);
                }
            }
        };

        Runnable runB = new Runnable() {
            public void run() {
                en.addItem("Hello!");
            }
        };

        try {
            //啟動第一個刪除元素的線程
            Thread threadA1 = new Thread(runA, "threadA1");
            threadA1.start();

            Thread.sleep(500);

            //啟動第二個刪除元素的線程
            Thread threadA2 = new Thread(runA, "threadA2");
            threadA2.start();

            Thread.sleep(500);
            //啟動增加元素的線程
            Thread threadB = new Thread(runB, "threadB");
            threadB.start();

            Thread.sleep(1000); // wait 10 seconds

            threadA1.interrupt();
            threadA2.interrupt();
        } catch ( InterruptedException x ) {}
    }
}

結果:
threadA1: threw an Exception!!!
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

這里發(fā)生了假喚醒,當添加完一個元素然后喚醒兩個線程去刪除纵东,這個只有一個元素粘招,所以會拋出數(shù)組越界,這時我們需要喚醒的時候在判斷一次是否還有元素偎球。
修改代碼:

  public String removeItem() throws InterruptedException {

        synchronized ( list ) {
            while ( list.isEmpty() ) {  //問題在這
                list.wait();
            }

            //刪除元素
            String item = (String) list.remove(0);
            return item;
        }
    }

等待/通知的典型范式

從上面的問題我們可歸納出等待/通知的典型范式洒扎。該范式分為兩部分,分別針對等待方(消費者)和通知方(生產(chǎn)者)衰絮。

等待方遵循原則如下:

  1. 獲取對象的鎖

  2. 如果條件不滿足袍冷,那么調用對象的wait()方法,被通知后仍要檢查條件

  3. 條件滿足則執(zhí)行對應的邏輯

對應偽代碼如下:

synchronized(對象){
    while(條件不滿足){
        對象.wait();
    }
    對應的處理邏輯
}

通知方遵循原則如下:

  1. 獲得對象的鎖

  2. 改變條件

  3. 通知所以等待在對象上的線程

對應偽代碼如下:

synchronized(對象){
    改變條件
    對象.notifyAll();
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猫牡,一起剝皮案震驚了整個濱河市胡诗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌淌友,老刑警劉巖煌恢,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異震庭,居然都是意外死亡瑰抵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門器联,熙熙樓的掌柜王于貴愁眉苦臉地迎上來二汛,“玉大人婿崭,你說我怎么就攤上這事∠捌叮” “怎么了逛球?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長苫昌。 經(jīng)常有香客問我颤绕,道長,這世上最難降的妖魔是什么祟身? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任奥务,我火速辦了婚禮,結果婚禮上袜硫,老公的妹妹穿的比我還像新娘氯葬。我一直安慰自己,他們只是感情好婉陷,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布帚称。 她就那樣靜靜地躺著,像睡著了一般秽澳。 火紅的嫁衣襯著肌膚如雪闯睹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天担神,我揣著相機與錄音楼吃,去河邊找鬼。 笑死妄讯,一個胖子當著我的面吹牛孩锡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亥贸,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼躬窜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了炕置?” 一聲冷哼從身側響起斩披,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讹俊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煌抒,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡仍劈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了寡壮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贩疙。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡讹弯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出这溅,到底是詐尸還是另有隱情组民,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布悲靴,位于F島的核電站臭胜,受9級特大地震影響,放射性物質發(fā)生泄漏癞尚。R本人自食惡果不足惜耸三,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浇揩。 院中可真熱鬧仪壮,春花似錦、人聲如沸胳徽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽养盗。三九已至缚陷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間爪瓜,已是汗流浹背蹬跃。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留铆铆,地道東北人蝶缀。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像薄货,于是被迫代替她去往敵國和親翁都。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容