Java多線程交叉打印ABABAB,一個線程打印A,一個線程打印B

在Java中想要完成此功能有好幾種方法都可以實現,這篇文章主要使用 wait 和 notifyAll 方法。

具體需求爲:

要求先打印字符 A ,再打印字符 B ,完了再打印字符 A …如此循環下去,要求格式爲:ABABABABABAB…

原理:

首先需要兩個線程,一個打印字符 A ,另一個打印字符 B ,那麼如何讓他們互相協作呢?此時,我需要一個 boolean 類型的變量 flag ,這個變量可以理解爲上次打印的字符是否是 A。如果變量爲 true ,就表示上次打印的字符是 A ,反之則打印的是 B。

當打印字符 A 的線程即將打印時,我需要先判斷這個變量 flag ,如果上次打印的是 A ,那麼我就讓這個線程進入 wait 狀態,不能讓它打印。如果讓它打印了,那麼結果就是 AA ,這種連續打印同一字符就是錯誤的。

同理,當打印字符 B 的線程即將打印時,我需要先判斷這個變量 flag ,如果上次打印的是 B ,那麼我就讓這個線程進入 wait 狀態。

緊接着,當判斷 flag 不成立的時候,那就說明可以打印,就走下面的打印流程就是了,打印完了要將 flag 修改爲對應的狀態。之後再調用 notifyAll 方法環境在等待隊列中等待的線程。

示例:

public class PrintABTest{

    // 該變量可以理解成:上一次打印是否是打印的字符 A。
    private volatile boolean flag = false;

    /**
     * 打印字符 A 的方法
     */
    private synchronized void printA(){
        try {
            // 判斷上一次打印是否是打印的 A,如果是就進行等待,如果不是就執行下面的代碼。
            while (flag){
                wait();
            }
            System.out.println("A");
            flag = true;
            // 喚醒在等待的線程
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * 打印字符 B 的方法
     */
    private synchronized void printB(){
        try{
            // 判斷上一次打印是否是打印的 B,如果是就進行等待,如果不是就執行下面的代碼。
            // 注意這裏是去反,因爲上次打印如果不是A,肯定就是B。
            while (!flag){
                wait();
            }
            System.out.println("B");
            flag = false;
            // 喚醒在等待的線程
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    /**
     * main 方法
     * 啓動線程調用打印方法
     */
    public static void main(String[] args) {
        // 創建實例
        PrintABTest printABTest = new PrintABTest();
        for (int i = 0; i < 300; i++) {
            // 打印 A
            new Thread(printABTest::printA).start();

            // 打印 B
            new Thread(printABTest::printB).start();
        }
    }
}

代碼很簡單,配合上述原理很好理解。下面思考兩個問題:

  1. 原理一直在說判斷 flag ,而判斷爲什麼要用 while 而不用 if,用 if 會有什麼效果?
  2. wait狀態的線程被喚醒後,會重新執行臨界區的代碼還是接着上次執行的地方接着往下執行。

先解答第二個問題,其答案是:進入wait狀態的線程被喚醒後,是接着上次執行的地方接着執行的。
先了解這個特性是爲了弄清楚第一個問題的前提。具體驗證過程可以參考:Java中進入wait狀態的線程被喚醒後會接着上次執行的地方往下執行還是會重新執行臨界區的代碼

而第一個問題,爲什麼要用while,是因爲當調用Java對象的notify()和notifyAll()方法時,會通知等待隊列(互斥鎖的等待隊列)中的線程,告訴它條件曾經滿足過
爲什麼說是曾經滿足過呢?因爲notify()只能保證在通知時間點,條件是滿足的。而被通知線程的執行時間點和通知的時間點基本上不會重合,所以當線程重新拿到執行權的時候,很可能條件已經不滿足了(保不齊有其他線程插隊)。換句話說就是當wait()返回時,有可能條件已經發生變化了,曾經條件滿足,但是現在已經不滿足了,所以要重新檢驗條件是否滿足,這就是爲什麼要使用 while 去檢查條件是否滿足。這一點需要格外注意!!!。

儘量使用notifyAll()

在上面的代碼中,我用的是notifyAll()來實現通知機制,爲什麼不使用notify()呢?這二者是有區別的,notify()是會隨機地通知等待隊列中的一個線程,而notifyAll()會通知等待隊列中的所有線程。從感覺上來講,應該是notify()更好一些,因爲即便通知所有線程,也只有一個線程能夠進入臨界區。但那所謂的感覺往往都蘊藏着風險,實際上使用notify()也很有風險,它的風險在於可能導致某些線程永遠不會被通知到。

假設我們有資源A、B、C、D,線程1申請到了AB,線程2申請到了CD,此時線程3申請AB,會進入等待隊列(AB分配給線程1,線程3要求的條件不滿足),線程4申請CD也會進入等待隊列。我們再假設之後線程1歸還了資源AB,如果使用notify()來通知等待隊列中的線程,有可能被通知的是線程4,但線程4申請的是CD,所以此時線程4還是會繼續等待,而真正該喚醒的線程3就再也沒有機會被喚醒了。

所以除非經過深思熟慮,否則儘量使用notifyAll()。


技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章