Java多執行緒/併發11、執行緒同步通訊:notify、wait

唐大麥發表於2017-04-28

假設有兩個執行緒,一個執行緒負責列印5次”Hello”,一個執行緒負責列印5次”Word”。現在提出一個要求,要求兩個執行緒交替列印,也就是要求Hello和Word交替出現。
我們先實現兩個執行緒列印字元的功能。程式碼如下:

package JConcurrence.Study;
public class ExecuteDemo {
    public static void main(String[] args) {
        final SyncLockTest LockTest = new SyncLockTest();
        /* 第一個執行緒說5次Hello */
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 5; i++) {
                    LockTest.Hello();
                }
            }
        }).start();
        /* 第二個執行緒說5次Word */
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 5; i++) {
                    LockTest.Word();
                }
            }
        }).start();
    }
}
/* 定義外部測試類SyncLockTest */
class SyncLockTest {
    public void Hello() {
        System.out.println(Thread.currentThread().getName() + "say:Hello");
    }
    public void Word() {
        System.out.println(Thread.currentThread().getName() + "say:World");
    }
}

執行結果:

Thread-0say:Hello
Thread-0say:Hello
Thread-0say:Hello
Thread-0say:Hello
Thread-0say:Hello
Thread-1say:World
Thread-1say:World
Thread-1say:World
Thread-1say:World
Thread-1say:World

可以看到結果並沒有交替出現。

這裡要強調兩點:
1、Hello()和Word()都沒有使用synchronized方法。因為每個方法中只有一條System.out.println語句,System.out屬於臨界資源,不會因為多個執行緒的競爭,而破化輸出。所以不會出現一個字串還沒有列印完時(如:只輸出前幾個字元的時侯),就被另一個執行緒搶奪的情況。所以System.out.println本身是執行緒安全的,不需要畫蛇添足的加上synchronized。

2、執行結果排得很整齊,感覺像是先執行完Thread-0,再執行Thread-1。千萬別被迷惑,那是因為計算機處理能力強大了。
如果在每個方法前加上sleep模擬耗時操作,就會看到,兩個程式不分先後的往外輸出。

public void Hello() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "say:Hello");
    }
    public void Word() {
        try {
            Thread.sleep(7);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "say:World");
    }

這時結果很零亂:

Thread-1say:World
Thread-0say:Hello
Thread-1say:World
Thread-0say:Hello
Thread-1say:World
Thread-1say:World
Thread-0say:Hello
Thread-1say:World
Thread-0say:Hello
Thread-0say:Hello

不過以上並不是本文重點。

現在開始說重點,我們如何實現兩個執行緒交替列印呢?這裡用到了同步對像鎖的wait()和 notify()方法。

wait()、notify()是定義在Object類裡的方法,可以用來控制執行緒的狀態。這三個方法最終呼叫的都是jvm級的native方法。隨著jvm執行平臺的不同可能有些許差異。
1、如果物件呼叫了wait方法就會使持有該物件的執行緒把該物件的控制權交出去,然後處於等待狀態。
2、如果物件呼叫了notify方法就會通知某個正在等待這個物件的控制權的執行緒可以繼續執行。如果有多個等待的執行緒,那麼會依靠JVM排程選出一個執行緒執行。
3、另外還有一個notifyAll方法,會通知所有等待這個物件控制權的執行緒繼續執行。

現在改造SyncLockTest類:
1、首先讓兩個執行緒方法成為有著相同鎖的synchronized同步方法,即在方法前加上synchronized關鍵字
2、新增兩個Boolen型別的標記,用於判斷當前哪個方法執行,哪個方法阻塞。
3、當標記不滿足當前方法執行的條件時,使用wait()對執行當前方法的執行緒阻塞。
4、在執行完當前方法的功能後,更新兩個Boolen標記值,同時呼叫notify()喚醒執行另一個方法的執行緒。

修改後的類程式碼如下:

/* 定義外部測試類SyncLockTest */
class SyncLockTest {
    /*
     * 為了增加程式碼可讀性,這裡用了兩個Boolean變數作為正在執行方法的標記
     * Hello_WillRun如果等於true,表明Hello()方法即將要獲得this鎖,否則就保持wait
     * 因為兩個方法是交替執行,同一時間只有一個執行,所以兩個變數必須保持互反:Hello_WillRun=!Word_WillRun
     * 預設將先執行的那個設為true,後執行的設為false
     */
    Boolean Hello_WillRun = true;
    Boolean Word_WillRun = false;
    /*兩個方法擁有同樣的鎖:this*/
    public synchronized void Hello() {
        /* 如果Hello()不是接下來將要執行的狀態,即:!Hello_WillRun,那麼保持等待wait() 
        while用於防止執行緒假醒後,順序往下執行輸出功能,從而破壞交替輸出*/
        while (!Hello_WillRun) {
            try {
                /* Hello()進行等待
                 * 呼叫wait()和notify()的物件必須和synchronized鎖物件一致,因此這裡用this*/
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /*執行Hello()核心功能*/
        System.out.println(Thread.currentThread().getName() + "say:Hello");

        /* Hello()執行完畢,設定下一步的標記狀態值 */
        Hello_WillRun = false;
        Word_WillRun = true;
        /*喚醒另一個執行緒*/
        this.notify();

    }
    /*兩個方法擁有同樣的鎖:this*/
    public synchronized void Word() {
        while (!Word_WillRun) {
            try {
                /* Word()進行等待
                 * 呼叫wait()和notify()的物件必須和synchronized鎖物件一致,因此這裡用this*/
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /*執行Word()核心功能*/
        System.out.println(Thread.currentThread().getName() + "say:World");
        /* Word()執行完畢,設定下一步的狀態值 */
        Hello_WillRun = true;
        Word_WillRun = false;
        /*喚醒另一個執行緒*/
        this.notify();

    }
}

成功輸出結果:

Thread-0say:Hello
Thread-1say:World
Thread-0say:Hello
Thread-1say:World
Thread-0say:Hello
Thread-1say:World
Thread-0say:Hello
Thread-1say:World
Thread-0say:Hello
Thread-1say:World

相關文章