多線程交替執行的一萬種寫法(記一道面試題)

多線程是 Java 的經典,也是重難點。很多時候,可能你反覆運行了你的代碼,確認沒有了問題。但是很可能線上運行的某一天,突然出錯掛了。事後反覆尋找原因,卻是死活重現不了場景。所以我們有必要深入地學習,不放過每一個細節。

題目

讓兩個線程依次打印 1A2B3C4D5E6F7G

誰都會想到的寫法

我特地把這個最常見的用法放在最前邊,由淺入深。也花了很多的篇幅,去描述這個最常見簡單的寫法,可能潛在的各種各樣的古怪的 很難重現 的問題。

很多其他寫法我放在了後邊,因爲最重要的是 夯實基礎。有了這些知識,就能很容易把握好每一種寫法。

這篇文章更多的是對多線程程序的 寫法可能出現的問題 去進行 邏輯分析,以及我們在編寫多線程程序需要 注意的各個點、細節,對很多底層的原理不會花過多詳盡的解釋,不然文章篇幅會過於長。
如果不懂 synchronized、volatile 等等原理的小夥伴可以去看我之前的一篇文章,裏面詳細介紹了線程 可見性、原子性 等內容,並從 虛擬機底層實現 分析了原理。
99%的人答不對的併發題(從JVM底層理解線程安全,硬核萬字長文)

wait notify

用 synchronized 的 wait 、notify 來實現線程間的通信保證數據的正確
(如果這你還不知道,就先去補補基礎吧)

public class T01_Wait_Notify {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // synchronized 的對象
    static Object o = new Object();
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o) {
                for(char c : array1) {
                    System.out.print(c);
                    o.notify();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (o) {
                for (char c : array2) {
                    System.out.print(c);
                    o.notify();
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t2").start();
    }
}

這個代碼段是有問題的,不知道細心的你有沒有看出來。
(你可以回頭再分析一下,看看哪裏會出差錯)

給你看一下演示結果:
demo
似乎乍一看,好像沒有啥問題。
你仔細看左邊,發現這個程序並沒有停止,而是繼續在運行着。(不管怎樣,它都不會正常終止)
我多運行幾次,你再看看,竟然出現了不同的結果:
demo
你會神奇地發現,打印的順序反了過來(從先數字後字母,變成了先字母后數字)
所以問題有兩個:

  • 程序不能正常終止
  • 輸出順序有時會出錯

其實問題原因並不複雜,關鍵是寫代碼時要很細心。很容易一不留神就有問題產生。
因爲很多時候,同一段程序,運行的結果可能會不同。(由於線程調度的不確定性,這是多線程很頭疼的地方)

我們繼續分析上面的問題。
首先是程序不停止:我們在循環結束後加一句 notify 即可

synchronized (o) {
    for(char c : array1) {
        // 省略上述代碼
    }
    o.notify(); // 循環結束後用notify
}

實際上,我們看上面的代碼,就會發現,兩個線程,在最終執行完所有的任務之後,最後都會調用 wait 方法。這樣,總有一個線程先執行結束,一個後執行結束。後結束的線程最後先調用 notify 喚醒先執行完的線程讓其退出,自己卻 wait 在了那裏。由於第一個線程已經終止退出了,所以沒有線程再去喚醒它,此時它就會永遠地在那裏等待。
在循環結束後都加一個 notify 方法,這樣保證了,不管哪一個線程先執行完,最後總歸會再調用一次 notify 方法,這樣就可以保證線程安全退出。

接下來我們再看第二個問題。我們雖然順序執行了:

new Thread().start();
new Thread().start();

調整執行順序

但是,線程調度不是程序員能手動控制的,所以兩個線程誰先誰後這是不能夠保證的。
所以誕生出這麼一種寫法,我們繼續研究:
一個線程先 wait,一個線程先執行。

public static void main(String[] args) {
    new Thread(() -> {
        synchronized (o) {
            for(char c : array2) {
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(c);
                o.notify();
            }
            o.notify();
        }
    }, "t2").start();
    new Thread(() -> {
        synchronized (o) {
            for (char c : array1) {
                System.out.print(c);
                o.notify();
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }, "t1").start();
}

一看執行結果,似乎很完美。(於是我接着執行幾次)
demo
(爲了測試出這個 bug,我點了幾十次,結果一直完好。。。於是我一生氣,寫了 10000 個循環,總算是出現死鎖了)
可見多線程的問題很容易隱藏其中,不被發現
demo
(不要在意打印順序,因爲線程不止兩個了,我開了循環重複了很多次纔有那麼一次出現死鎖)
(當然也可以用 sleep 來進行測試)

現在我們分析代碼:
其實我們的初衷是好的,讓一個線程先等待,一個線程先執行。但是由於線程調度的不確定性,很可能一個線程先去執行並且 notify 了,然後這個線程纔開始 wait,導致死鎖。
所以就可能在某一次的執行過程中,程序在開始的第一步,纔打印了一個字符,就兩個線程就雙雙陷入等待。

所以之前的不管哪一種寫法,都需要保證一點,就是兩個線程的開始執行,必須保證先後次序。
第一種寫法雖然可能打印相反,但是至少不會死鎖。
第二種寫法只要執行成功就一定打印正確,但是可能出現死鎖。

僞喚醒

要保證線程的執行順序,有很多種寫法。可以用循環 CAS,LockSupport,CountDownLunch 等等。
不過我更推薦用 while 循環加 狀態判斷 的方式
因爲在 JDK 的官方的 wait() 方法的註釋中明確表示線程可能被 “虛假喚醒“,JDK 也明確推薦使用while 來判斷狀態信息。

這時,如果一個線程剛執行完任務,需要 另一個線程執行,它調用了 notify 將其喚醒,然後 wait 釋放鎖,開始等待……
重點來了: 結果它纔開始等,就不知被誰叫醒了(僞喚醒),此時它又開始拿鎖執行,如果搶到了鎖,相當於連續執行了兩次!!
另一個線程就只好等到它再一次 wait 釋放鎖時才能執行它的任務。

這個我就不測試了,因爲概率實在太低了。。。所以很可能你拿幾萬次循環去跑都發現不了問題,以爲完全正確了。(然而它暗暗地藏着問題,等上線了某一天就讓你係統掛掉,然後你還找不到問題,因爲錯誤也重現不了)
(太可怕了!!)

最終如下修改:
由於有一個狀態來記錄,所以就算是其它線程意外搶到了鎖,也會放棄執行機會,讓出鎖讓其他線程執行。

public class T03_Wait_Notify {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // synchronized 的對象
    static Object o = new Object();
    // 添加一個狀態來記錄應該由誰打印 !!!!!!!!!!!!!
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (o) {
                for(char c : array1) {
                    // 循環保證不管什麼時候意外獲得了鎖,只要狀態不對都給打回去
                    while (1 != status) {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 2; // 執行完了之後修改標誌位
                    o.notify();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            synchronized (o) {
                for (char c : array2) {
                    // 循環保證不管什麼時候意外獲得了鎖,只要狀態不對都給打回去
                    while (2 != status) {
                        try {
                            o.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 1; // 執行完了之後修改標誌位
                    o.notify();
                }
            }
        }, "t2").start();
    }
}

至少我暫時還是沒出錯過,而且真的找不出錯了(要是真的還能有問題,請在評論區告訴我)
demo

wait notify 探究

有一個問題,爲什麼 wait notify 是 Object 的方法,而不是線程的方法?(因爲 wait 是線程去等待,notify 喚醒的也是線程。這是一個非常矛盾的點)
首先,wait notify 這兩個方法設計出來本身就是用來作爲線程間通信使用的,而不是爲了阻塞和喚醒線程。
實際上,曾經的 JDK 允許用 suspend() 和 resume() 方法來阻塞和喚醒線程,不過後來就被廢棄了。
demo
而且我們也知道,Thread 類的 stop() 方法也是被廢棄的。

因爲本質上直接對指定的線程去操作是線程不安全的 !!!

因爲線程調度的不確定性,我們去對一個線程直接操作的時候,往往很難保證線程當時的執行狀態,所以直接對指定線程去操作很容易破壞線程的執行狀態,造成程序異常。
所以我們很常用的 sleep() 方法是 Thread 類的 static 方法,而不是直接去操作 Thread 對象,讓其睡眠。

而且 suspend 和 resume 方法是一個遊離的不受管理控制的方法,很容易造成 死鎖,而 synchronized 同步之後,在 同步鎖 的保護之下,wait notify 就不容易出現死鎖的情況。
因爲在 synchronized 的同步保護下,一個線程的 notify方法雖然會喚醒線程,但是線程要進入同步塊內,必須等到另一個線程釋放鎖纔行,所以等到那個線程 wait 會釋放鎖,它才能夠繼續往下執行。
而用 suspend 和 resume 的方法,在 t1 喚醒 t2 之後,然後讓 t1 進入等待,可是由於線程調度的不確定性,在 t1 進入等待之前,可能 t2 已經執行完了它的邏輯並且喚醒 t1,然後進入等待。那麼這時 t1 才進入等待,那麼它們就會雙雙睡去,程序卡死。
demo
在虛擬機內部,線程的 wait 與 notify 實際上是由一個對象監視器(Object Monitor)來管理的。
點開 Hotspot 虛擬機源碼,我們看到:
objectmonitor
我們在 Hotspot 源碼裏也可以看到,對象監視器中有一個 WaitSet,就是我們常說的等待池。線程通過這些方法來通信,都是由 synchronized 所指定的對象的對象監視器來管理。
一個線程調用 Object 的 wait 方法,則會被加入到該對象的對象監視器的等待池中,而另一個線程調用 Object 的 notify 方法,則會喚醒等待池中的一個線程。
所以從虛擬機底層實現來說,wait notify 也必須是 Object 的方法,而不能直接操作線程。

文章目錄位置說明

至於爲什麼我把文章目錄放在這裏,也是出於我的 良苦用心(防止有些小夥伴跳躍瀏覽)

有些小夥伴可能看到了標題,就想要瘋狂的複製各種寫法,然後去根面試官裝 X。然而事實上,我給出的寫法雖然較多,但是隻要你願意去想,你可以繼續用更多的工具類去實現,或者設計出更優秀的算法。越往後面的各種寫法,都更多的只是擴展一下知識面,以及鍛鍊思維的靈活性。

實際上,其他花裏胡哨的寫法,追根溯源,原理都是一樣的。所以我們應該把最重要的,對於這類併發編程的 原理、思維、錯誤、細節 全部理解透徹,我們在實際中做到遊刃有餘。

LockSupport

我把這個放在第二,因爲它是同樣很重要的一個知識點。
最主要的原因是它的 優秀,即使程序員不夠小心,它也能自己避免掉一些多線程的危險情況。

park、unpark

可能見過了之前出現過的各種錯誤和問題,你們已經很害怕了,非常謹慎地先照着之前 wait、notify 的程序去寫。
我先給大家看一個示例

public class T04_LockSupport {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // main
    public static void main(String[] args) {
        Thread t1;
        Thread t2;
        t1 = new Thread(() -> {
            for(char c : array1) {
                System.out.print(c);
                LockSupport.unpark(t2); // 叫醒t2
                LockSupport.park();     // 阻塞自己
            }
        });
        t2 = new Thread(() -> {
            for(char c : array2) {
                LockSupport.park();     // 阻塞自己
                System.out.print(c);
                LockSupport.unpark(t1);// 叫醒t1
            }
        });
        t1.start();
        t2.start();
    }
}

你可以先看一下有沒有問題(我總感覺一般我這麼問你們就算看不出有問題也覺得有問題)
實際上,你不用考慮是不是順序死鎖等問題。。
因爲編譯就會報錯。。
demo
所以,這些代碼看似簡單,卻很容易體現出一個程序員的紮實功底和細節辨析能力。
創建 t1 的時候還沒有 t2,它裏面的方法卻用到了 t2,所以會出錯。

public static void main(String[] args) {
    Thread t1 = null;
    Thread t2 = null;
    // 省略後面代碼
}

這樣是否會出錯呢?仔細想想
答案是:
demo
不要崩潰,每出錯一次,你就會學習到一點知識,積少成多,你就變成大牛。
上面說 lambda 表達式不行,那就再換

t1 = new Thread(){
    @Override
    public void run() {
        for(char c : array1) {
            System.out.print(c);
            LockSupport.unpark(t2); // 叫醒t2
            LockSupport.park();     // 阻塞自己
        }
    }
};

這次不用 lambda 表達式了,用重寫 run 方法
答案是。。。。
demo
沒關係,這裏肯定會出錯的。
因爲實際上,對於內部類引用的本地變量,必須是 final 的,這是 Java 語言所規範的,否則就會報錯。
而 lambda 表達式本質上就是 匿名內部類,所以也必須遵循規範。

所以這個 Thread 不能寫在方法的局部變量中,而是需要把它放到成員變量裏去。
經修改:

public class T05_LockSupport {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // 兩個線程
    static Thread t1 = null;
    static Thread t2 = null;
    // main
    public static void main(String[] args) {
        t1 = new Thread(() -> {
            for(char c : array1) {
                System.out.print(c);
                LockSupport.unpark(t2); // 叫醒t2
                LockSupport.park();     // 阻塞自己
            }
        });
        t2 = new Thread(() -> {
            for(char c : array2) {
                LockSupport.park();     // 阻塞自己
                System.out.print(c);
                LockSupport.unpark(t1); // 叫醒t1
            }
        });
        t1.start();
        t2.start();
    }
}

這樣是否會出現死鎖?

編譯不報錯了,我們再來探討死鎖問題。
經過上面 wait notify 的摧殘,相信你們一下就能看出來。
要是 t1 先 unpark t2,然後 t2 再 park 自己,那這樣就會雙雙陷入等待,程序永遠不會結束。

然而事實上,不可能!!!

你可能要懵逼了,爲啥這樣不會出現死鎖?
所以我在開頭說 LockSupport 是非常優秀的,它會自己避免掉一些死鎖情況,防止一些經驗不足的程序猿總是寫出漫天 bug 的代碼。

LockSupport 底層是使用了 Unsafe(你不瞭解也沒事)。首先當一個線程被 unpark 的時候,如果它當時還沒有 park 住,它會記住它已經被 unpark 過了,這樣下次再被 park 時,它會就會自動解除 park。這樣就防止了先被 unpark 而導致死鎖情況。

僞喚醒

和 wait notify 一樣,LockSupport 的 park() 方法仍然存在僞喚醒的情況,所以我們也同樣需要一個 while 循環來判斷。
(這裏與 wait notify 類似,大家應該很容易明白)

public class T06_LockSupport {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // 兩個線程
    static Thread t1;
    static Thread t2;
    // 添加一個狀態來記錄應該由誰打印
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        t1 = new Thread(() -> {
            for(char c : array1) {
                while (1 != status)
                    LockSupport.park(); // 阻塞自己
                System.out.print(c);
                status = 2;
                LockSupport.unpark(t2); // 叫醒t2
            }
        });
        t2 = new Thread(() -> {
            for(char c : array2) {
                while (2 != status)
                    LockSupport.park(); // 阻塞自己
                System.out.print(c);
                status = 1;
                LockSupport.unpark(t1); // 叫醒t1
            }
        });
        t1.start();
        t2.start();
    }
}

Lock Condition

既然你會用 synchronized 的 wait notify,那我相信你也一定會用 Lock 接口。
Lock 接口可以說實現並擴展了 synchronized 關鍵字(synchronized 可以實現的 Lock 都可以實現,並且 synchronized 不能實現的,Lock 也可以實現)。

(而且之後我不會再寫很多理論知識了,因爲 最重要 的那些點就是我 前兩個大主題 所講的知識,後面的更多是擴展知識面)

類似於 wait notify

(基本用法我這裏不會講,不會的要自己去乖乖補課哦)
(不同之處我用 註釋 標出來了,關注不同點即可,就不用整片代碼看了)

public class T07_Lock_Condition {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // Lock Condition
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    // 狀態標誌
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock(); // 區別是顯示加鎖解鎖
            try {
                for(char c : array1) {
                    while (1 != status) { // 與之前synchronized的while類似
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c); // 可以看出代碼實際上沒太大變化
                    status = 2;
                    condition.signal();
                }
            } finally {
                lock.unlock(); // 區別是顯示加鎖解鎖
            }
        }, "t1").start();
        new Thread(() -> {
            lock.lock(); // 區別是顯示加鎖解鎖
            try {
                for(char c : array2) {
                    while (2 != status) { // 與之前synchronized的while類似
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c); // 可以看出代碼實際上沒太大變化
                    status = 1;
                    condition.signal();
                }
            } finally {
                lock.unlock(); // 區別是顯示加鎖解鎖
            }
        }, "t2").start();
    }
}

雙 Condition

上面的寫法,與我們改進後的 wait notify 是沒有區別的,而且也不會出錯。
不過,爲了展現出 Lock 與 synchronized 不同的高級用法,可以創建出兩個 Condition。t1 在一個 condition 等待,t2 在另一個 Condition 等待,這樣就可以分別指定喚醒哪一個 Condition 對應的線程。
不過此處由於只有兩個線程,同一時刻永遠只有一個線程在等待,所以一個 Condition 已經足夠。不過,如果線程數量增多,我們就可以利用到多 Condition 的優勢了。
(不同之處我用 註釋 標出來了,關注不同點即可,就不用整片代碼看了)

public class T08_Lock_Condition {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // Lock Condition
    static Lock lock = new ReentrantLock();
    static Condition condition1 = lock.newCondition(); // 兩個condition
    static Condition condition2 = lock.newCondition();
    // 狀態標誌
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            lock.lock();
            try {
                for(char c : array1) {
                    while (1 != status) {
                        try {
                            condition1.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 2;
                    condition2.signal(); // 喚醒condition2的t2線程
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        new Thread(() -> {
            lock.lock();
            try {
                for(char c : array2) {
                    while (2 != status) {
                        try {
                            condition2.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(c);
                    status = 1;
                    condition1.signal(); // 喚醒condition2的t1線程
                }
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

自旋

觀察之前的代碼我們可以發現,在 t1 結束後需要 t2 執行的時候,都是阻塞住自己,等 t2 執行完了再喚醒。
我們都知道,阻塞與喚醒一個線程因爲涉及到用戶態與內核態的切換,需要做很多現場保留還原、大量的驗證等等一系列底層的複雜操作,所以會影響到程序的性能。因此,對於一些加鎖內部代碼很簡單的小程序,我們完全不需要用鎖將其阻塞,可以用 自旋 來保證安全。

while + 判斷

public class T09_CAS {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // 狀態標誌
    volatile static int status = 1;
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            for(char c : array1) {
                while (1 != status) {
                    // 空循環 等到另一個線程執行完修改狀態
                }
                System.out.print(c);
                status = 2;
            }
        }, "t1").start();
        new Thread(() -> {
            for(char c : array2) {
                while (2 != status) {
                    // 空循環 等到另一個線程執行完修改狀態
                }
                System.out.print(c);
                status = 1;
            }
        }, "t2").start();
    }
}

yield

不過之前的代碼仍有些小小的不足。就是在 while 循環的時候,會消耗 CPU,它必須一直等待線程調度到另一個線程執行結束,它才能結束循環。
所以我們可以在 while 循環時加一個 yield() 方法,讓它主動讓出 CPU 去線程調度。雖然線程調度有不確定性,可能它讓出 CPU 之後,仍然又被派回來繼續執行。但是這樣做仍可以增加提前結束循環的機會,節省一部分 CPU 資源。

while (1 != status) {
    Thread.yield();
}

BlockingQueue

可以用 BLockingQueue 阻塞隊列來實現,一個線程執行結束,往隊列中 put,另一個線程取到就繼續。
這樣可以保證安全,就比如一個蘋果,你給他,他在給你,同一時刻總只可能有一個人拿着蘋果。
示例:

public class T11_BlockingQueue {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // BlockingQueue q1用來t2塞給t1東西 q2用來t1塞給t2東西
    static BlockingQueue<Object> q1 = new ArrayBlockingQueue<>(1);
    static BlockingQueue<Object> q2 = new ArrayBlockingQueue<>(1);
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            for(char c : array1) {
                System.out.print(c);
                try {
                    q2.put(new Object());
                    q1.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            for(char c : array2) {
                try {
                    q2.take();
                    System.out.print(c);
                    q1.put(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }
}

TransferQueue

TransferQueue 繼承了 BlockingQueue 並擴展了一些新方法。生產者會一直阻塞直到所添加到隊列的元素被某一個消費者所消費(不僅僅是添加到隊列裏就完事,可以說是專門爲消息傳遞而誕生的)。

public class T13_TransferQueue {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // TransferQueue
    static TransferQueue<Object> q1 = new LinkedTransferQueue<>();
    static TransferQueue<Object> q2 = new LinkedTransferQueue<>();
    // main
    public static void main(String[] args) {
        new Thread(() -> {
            for(char c : array1) {
                try {
                    System.out.print(c);
                    queue.transfer(new Object());
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            for(char c : array2) {
                try {
                    queue.take();
                    System.out.print(c);
                    queue.transfer(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }
}

PipedStream

很多小夥伴可能沒有聽說過 PipedStream 這麼一個東西。不過這沒有關係,因爲這本來就沒啥大用。。。。
這裏說到這個呢僅僅是因爲擴充一下知識面,到時候如果你在面試,對於這同一道面試題,你能寫出那麼多種的寫法,自然會被面試官高看。

PipedInputStream、pipedOutputStream 都是輸入輸出流,是同步阻塞的,所以性能上比較差勁。而且線程的通信完全不需要這玩意,用共享變量,並且保證操作的線程安全即可。(這個僅僅是擴充下知識面)

public class T12_PipedStream {
    // 用於打印的字符
    static char[] array1 = "1234567".toCharArray();
    static char[] array2 = "ABCDEFG".toCharArray();
    // PipedStream
    static PipedInputStream in1 = new PipedInputStream();
    static PipedInputStream in2 = new PipedInputStream();
    static PipedOutputStream out1 = new PipedOutputStream();
    static PipedOutputStream out2 = new PipedOutputStream();
    // main
    public static void main(String[] args) throws IOException {
        in1.connect(out1);
        in2.connect(out2); // 需要把input和output連接起來
        new Thread(() -> {
            for(char c : array1) {
                try {
                    System.out.print(c);
                    out2.write(2); // 寫給t2
                    out2.flush();
                    in1.read();    // t1讀
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        new Thread(() -> {
            for (char c : array2) {
                try {
                    in2.read();    // t2讀
                    System.out.print(c);
                    out1.write(1); // 寫給t1
                    out1.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }
}

題外話

實際上這道面試題最開始只是一道填空題,在 synchronized 的 wait 和 notify 方法位置將這兩個代碼挖掉,讓同學們去填空,所以其實是很簡單的。
但是,實際上,如果我們繼續去深入研究這個知識,你會發現其實有很多知識點都可以延伸到,我們從中可以學習到很多的知識。
當然,最主要的,是要把我最上邊寫的最基本的 synchronized 的 wait notify 學習掌握透徹,理解在多線程中會產生的各種問題。那麼,其他的各種寫法對於你來說也一定是遊刃有餘了。

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