標籤:write 最大 主線程 線程 \n not 文章 put 隊列實現
上篇我們講了使用wait()和notify()使線程間實現合作,這種方式很直接也很靈活,但是使用之前需要擷取對象的鎖,notify()調用的次數如果小於等待線程的數量就會導致有的線程會一直等待下去。這篇我們講多線程間接協作的方式,阻塞隊列和管道通訊,間接協作的優點是使用起來更簡單並且不易出錯。
阻塞隊列
阻塞隊列提供了一種功能,即你可以在任何時刻向隊列內扔一個對象,如果隊列滿了則當前線程阻塞;在任何時刻都可以從隊列中取出一個對象,如果隊列為空白則當前線程阻塞。阻塞隊列是安全執行緒的,使用它時無需加鎖。此外其內部是使用顯示鎖實現的同步,使用Condition實現的線程阻塞。阻塞隊列的介面是BlockingQueue,它有兩個實作類別:
1. ArrayBlockingQueue:底層使用數組實現的隊列,有固定長度,調用其構造方法時必須提供隊列的最大長度。
2. LinkedBlockingQueue:底層使用鏈表實現的隊列,理論上講是沒有最大長度的,使用時不用提供隊列長度;但實際上這個隊列的長度不能超過Integer.MAX_VALUE。
這兩個類使用的時候沒有太大區別,我們以LinkedBlockingQueue為例,重寫“學生去食堂打飯”的例子,代碼如下:
class Student implements Runnable { private Object wan = new Object(); public void run() { try { System.out.println("學生:取到了一個碗"); BlockingQueueTest.wanQueue.put(wan); System.out.println("學生:阿姨幫忙盛飯"); wan = BlockingQueueTest.wanWithFanQueue.take(); System.out.println("學生:吃飯"); } catch (InterruptedException e) {} }}class CafeteriaWorker implements Runnable { public void run() { try { Object wan = BlockingQueueTest.wanQueue.take(); System.out.println("阿姨:給學生盛飯"); BlockingQueueTest.wanWithFanQueue.put(wan); } catch (InterruptedException e) {} }}public class BlockingQueueTest { public static BlockingQueue wanQueue = new LinkedBlockingQueue(); public static BlockingQueue wanWithFanQueue = new LinkedBlockingQueue(); public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new Student()); exec.execute(new CafeteriaWorker()); exec.shutdown(); }}
輸出結果如下:
學生:取到了一個碗
學生:阿姨幫忙盛飯
阿姨:給學生盛飯
學生:吃飯
在這個例子中我們定義了兩個隊列,一個是空碗的隊列,另一個是盛完飯的碗的隊列。“學生線程”取到碗後將空碗放入wanQueue隊列,然後試圖從wanWithFanQueue隊列中取出盛好的飯碗;“阿姨線程”試圖從wanQueue隊列中取出空碗,然後將盛好的飯碗放到wanWithFanQueue隊列中。上次我們使用wait()方法時必須要求“阿姨線程”先啟動,否則會導致“阿姨線程”錯過學生的訊號,而使用阻塞隊列實現時我們就不再要求兩個線程的啟動順序了,使用阻塞隊列規避了錯失訊號的風險。有的同學可能會好奇為什麼會使用兩個隊列,這是因為如果使用同一個隊列,同學線程把碗扔進隊列後,可能“阿姨線程”沒來得及取出來就被“同學線程”拿回去了,感興趣的同學可以自行測試。
管道通訊
通過管道的方式也可以使線程間實現互動,管道和阻塞隊列類似,當管道內沒有資料的時候,如果某個線程嘗試去讀取資料就會被阻塞。
我們可以使用PipedWriter和PipedReader來實現對管道資料的讀取和寫入。和阻塞隊列不同的是,阻塞隊列中不同線程都是操作一個隊列的對象;使用管道時,不同的線程可以使用不同的對象,只要將它們註冊為一個管道即可。
我們使用管道通訊類比一個線程對另一個線程表白,代碼如下:
class Sender implements Runnable { private PipedWriter writer; Sender(PipedWriter writer) { this.writer = writer; } public void run() { String str1 = new String("I love you\n"); String str2 = new String("Do you love me\n"); try { writer.write(str1.toCharArray()); writer.write(str2.toCharArray()); } catch (IOException e) {} }}class Receiver implements Runnable { private PipedReader reader; public Receiver(PipedReader reader) { this.reader = reader; } public void run() { try { while(true) { char c = (char)reader.read(); System.out.print(c); } } catch (IOException e) {} }}public class PipeCommunication { public static void main(String[] args) throws Exception { PipedReader reader = new PipedReader(); PipedWriter writer = new PipedWriter(reader); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new Sender(writer)); exec.execute(new Receiver(reader)); Thread.sleep(1000); exec.shutdownNow(); }}
運行後輸出結果如下,一秒後程式退出:
I love you
Do you love me
我們在主方法裡先定義了一個PipedReader對象,然後將這個對象作為PipedWriter的構造方法的參數傳給PipedWriter對象,這樣就實現兩個輸入輸出資料流的綁定,分別將兩個流對象傳給兩個線程對象。在資訊的接收方我們使用一個死迴圈讓其不斷的從管道內讀入,從輸出結果可以看出read()方法在管道內沒有資料的時候被阻塞了,因為輸出結果沒有迴圈列印其它字元。此外主線程sleep一秒後調用了shutdownNow()方法,這個方法向所有運行著的線程發送中斷訊號,程式運行一秒後就退出了,我們可以看出中斷訊號打斷了Receiver的阻塞狀態,由此得出結論:管道類阻塞時可以被中斷訊號打斷。
總結
本篇講了使用阻塞隊列和管道來實現線程間的合作,相對於使用wait()協作而言這兩種方式更為進階,使用起來更容易而且不易錯,此外阻塞隊列和管道都是安全執行緒的,因此使用它們的時候不需要使用鎖。需要實現線程間協作時可以根據實際需要,權衡利弊進行選擇。
公眾號:今日說碼。關注我的公眾號,可查看連載文章。遇到不理解的問題,直接在公眾號留言即可。
Java並發編程(九)線程間協作(下)