多線程學習總結(十一)——線程安全之線程間的通信notify和notifyAll

    聲明:文章內容全都是自己的學習總結,如有不對的地方請大家幫忙指出。有需要溝通交流的可加我QQ羣:425120333
    關於notify和notifyAll這兩個方法的區別從方法的命名上就能看出來,一個是喚醒在對象上等待的一個阻塞線程,一個是喚醒在對象上等待的所有線程。
再多數情況下我們習慣性的使用notifyAll方法,因爲能保證不出錯,而使用了notify方法可能在沒有考慮清楚的情況下就讓所有線程都死掉了,
從而使得進程雖然在運行,其實已經沒動作了。 
    那既然notifyAll能保證不出錯了,爲啥還要有notify,意義在哪?其實使用notify是出於性能或者是效率的考慮。舉一個簡單的例子:
買票問題,因爲只有一個窗口,那麼在同一時刻最多隻有一個人能買到票,這個人買完後要通知其他人我買好了,使用notify相當於只是告訴一個人我買好了,
只有一個人知道,其他人還是等着的,然後這個人去買票,買好後通知下一個,直到所有人都買好。而使用notifyAll相當於一個人買好票後,拿出喇叭喊了一聲
我買好了,然後走了,其他等待的都知道這個人買好了,都想輪到自己買,
大家就都去搶這個機會,當然每次都是隻有一個人能搶到,然後買完,再喊一聲,直到大家都買完。這麼一講,大家應該能明白其中的區別了。
    雖然使用notify相對來說效率會高一些,但是容易出錯,看下面一個例子:
public class TestNotify {
    public static void main(String[] args) {
        PlateTest plateTest = new PlateTest();

        //用多個線程
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Producer(plateTest));
            t.start();
        }

        //多個線程啓動
        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(new Customer(plateTest));
            t1.start();
        }
    }
}

class PlateTest {
    private List<String> list = new ArrayList<String>();

    public synchronized void put() {
        while (list.size() > 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        list.add("蘋果");
        System.out.println("往容器中放入一個成功,現在的大小爲: " + list.size() + ",內容爲:" + list.get(0));
        this.notify();
    }

    public synchronized void get() {
        while (list.size() == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        String str = list.get(0);
        list.remove(str);
        System.out.println("從容器中取出一個成功,現在的大小爲: " + list.size() + ",取出內容爲:" + str);
        this.notify();
    }
}

class Producer implements Runnable {
    PlateTest plateTest;

    public Producer(PlateTest plateTest) {
        this.plateTest = plateTest;
    }

    @Override
    public void run() {
        for (;;) {
            plateTest.put();
        }
    }
}

class Customer implements Runnable {
    PlateTest plateTest;

    public Customer(PlateTest plateTest) {
        this.plateTest = plateTest;
    }

    @Override
    public void run() {
        for (;;) {
            plateTest.get();
        }
    }
}

這段代碼和前面多線程學習總結(九)中的代碼基本類似沒什麼區別,唯一的不同就是在main方法中使用了多個線程啓動,而不是單個,而問題也是
很明顯,啓動的線程只要是大於一,就有可能使得代碼運行不下去(不是越多就越快運行不下去),如果改爲notifyAll就不會出現這種情況。

    使用notify運行不下去的模擬分析(notify丟失的解釋):
    上面問題該怎麼解釋呢,我給大家一步步分析,因爲加了synchronized關鍵字,所有同步塊的內容在一個時間點只能有一個線程執行,這是前提條件。
現在假定啓動了2個producer線程,分別爲p1和p2,2個customer線程,分別爲c1和c2,然後2個customer先執行,因爲發現list.size() == 0 
所以這c1和c2都wait(都是在plateTest對象上wait)然後p1執行將list.size()改爲1,然後喚醒一個在plateTest的線程,暫定爲c1。
這時wait的線程只有c2,然後運行的是其他三個, list.size() =1;接着p1執行,發現list.size() ==1,p1就wait了,同理p2也wait了,然後c1運行,
能正常運行,喚醒一個(這時wait的總共有三個c2,p1,p2),剛好是c2;這時運行着的是c1和c2,wait的是p1和p2,list.size() =0;然後c1和c2相繼運行,
發現list.size() ==0就都wait了,這時四個線程都wait了,沒有一個在運行,所以導致程序就運行不下去了。
    有人可能會說剛好出現這種情況也太巧了,但是不管怎麼說有這個可能就能讓你的程序運行不下去,說明代碼有問題。通過上面的分析,也明白了爲啥用
notifyAll不會有問題了,畢竟每次都是喚醒所有在wait上的線程。大家可以結合http://www.cnblogs.com/lnlvinso/p/4753554.html這個鏈接上的內容
加深一下理解。

    關於上面的代碼除了用notifyAll實現之外還有另一種辦法就是在使用wait()方法之前調用notify(),代碼示例如下:
 try {
                System.out.println(Thread.currentThread().getName() + "will wait");
                this.notify();
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
但因爲notify選擇喚醒其中一個線程。選擇是任意性的,由具體實現做出決定.因此設計JAVA程序時,不要依賴於這一點.上面的代碼我執行後發現雖然保證了至少
有一個是一致運行的,但總會出現p1喚醒p2,p2又喚醒p1,一直在循環,好久才喚醒c1,可是c1又喚醒c2,然後c2又喚醒c1的情況,所以這種方法還不如用notifyAll
    在這裏提出,只是給大家一個解決的思路,真正開發時不要這樣處理。notify的真正使用場景還是喚醒哪一個都可以沒有影響的情況下,這時notifyAll
雖然也可以,但是notify的效率會更高。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章