場景
當家庭主婦在廚房炒菜時,門鈴響了,快遞員送來快遞,由於炒菜你不能馬上去開門,而是讓快遞員在門口等一會兒,才去打開門。
guarded 保護的意思
Suspension 暫停延遲的意思
下面我們來模擬一下這個場景,快遞員的職責是發出收快遞的請求,家庭主婦的職責主要是接收請求並處理
代碼實現
我們先來實現請求類,比如快遞員的請求是接收快遞,快遞員可能來自順豐,可能來自圓通,可能來自京東,家庭主婦可能在不同平臺買了多種商品,分別使用這些快遞
<code>public class Request { private final String name; public Request(String name) { this.name = name; } public String getName() { return name; } public String toString() { return "[ Request " + name + " ]"; } }/<code>
如果同時有多個快遞員同時來家裡送快遞,此時家庭主婦可能讓他們都等一下,因此我們需要一個存放請求的類,把快遞員的請求都保存起來,然後依次處理
<code> import java.util.LinkedList; import java.util.Queue; public class RequestCollection { private final Queue queue = new LinkedList(); public synchronized Request getRequest() { while (queue.peek() == null) { try { wait(); } catch (InterruptedException e) { } } return queue.remove(); } public synchronized void putRequest(Request request) { queue.offer(request); notifyAll(); } } /<code>
說明:
Queue:隊列,先進先出,LinkedList實現了Queue接口
peek方法:如果有元素就返回頭元素,如果沒有,返回null;該方法不會移除元素
remove方法:如果有元素就返回頭元素,否則拋出異常
offer方法:添加元素到隊列尾
getRequest方法:如果隊列中沒有請求,就wait;如果有就獲取隊列頭元素;peek方法會判斷隊列中是否有請求,如果沒有就進入wait,peek方法是保護條件,如果不滿足則等待
等待的對象是:請求隊列中是否有請求,等待請求隊列是否發生變化,導致其發生變化的是將請求放入隊列中,將請求從隊列中取出
請求的結構:
while(守護條件的邏輯非){
使用wait進行等待
}
執行後續處理
注意這裡使用while循環,思考下是否可以使用if
如果使用if,也會進入到wait等待,問題在於如果在wait被打破進入後續處理之前,其他線程將請求處理完的話,問題就出現了,所以這裡要使用while
putRequest方法:將請求放入隊列的尾部;
快遞員發出請求
<code> import java.util.Random; public class Courier extends Thread{ private final Random random; private final RequestCollection requestCollection; public Courier(RequestCollection requestCollection, String name, long seed) { super(name); this.requestCollection = requestCollection; this.random = new Random(seed); } public void run() { for (int i = 0; i < 10000; i++) { Request request = new Request("請求編號: " + i); System.out.println(Thread.currentThread().getName() + " 發出請求 " + request); requestCollection.putRequest(request); try { Thread.sleep(random.nextInt(1000)); } catch (InterruptedException e) { } } } }/<code>
處理請求的家庭主婦類
<code> import java.util.Random; public class Mother extends Thread{ private final Random random; private final RequestCollection requestCollection; public Mother(RequestCollection requestCollection, String name, long seed) { super(name); this.requestCollection = requestCollection; this.random = new Random(seed); } public void run() { for (int i = 0; i < 10000; i++) { Request request = requestCollection.getRequest(); System.out.println(Thread.currentThread().getName() + " 處理請求 " + request); try { Thread.sleep(random.nextInt(1000)); } catch (InterruptedException e) { } } } }/<code>
主函數類
<code> public class Main { public static void main(String[] args) { RequestQueue requestQueue = new RequestQueue(); new ClientThread(requestQueue, "Alice", 3141592L).start(); new ServerThread(requestQueue, "Bobby", 6535897L).start(); } }/<code>
模式解讀
1、和臨界區模式的區別
兩種模式都使用synchronized,但是GuardedSuspension添加了判斷條件
2、注意wait和notify的使用
如果沒有notify,那麼會一直等待。
可以設置wait超時
3、可以不使用wait
這樣的話一直執行while循環,此時可以使用yield方法讓出執行時間
while(邏輯判斷非){
Thread.yield();
}
這叫busy wait
LinekdBlockingQueue
下面我們使用LinkedBlockingQueue來改造上面的程序
修改RequestCollection類
<code> import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class RequestCollection { private final BlockingQueue queue = new LinkedBlockingQueue(); public Request getRequest() { Request req = null; try { req = queue.take(); } catch (InterruptedException e) { } return req; } public void putRequest(Request request) { try { queue.put(request); } catch (InterruptedException e) { } } } /<code>
說明:
1、上面的take和put方法做了互斥處理,因此這裡不需要使用synchronized關鍵字
問題:
將RequestQueue中的putRequest方法中兩條語句的順序顛倒過來,即如下:
<code> public synchronized void putRequest(Request request) { notifyAll(); queue.offer(request); }/<code>
先運行notifyAll,程序是否正確?
答案:正確,因為調用notifyAll後,線程仍舊持有鎖,其他線程雖然被喚醒,但是馬上會阻塞(不會做條件判斷),只有在該方法調用後,線程釋放鎖,其他線程才有機會執行。
但是最好將notifyAll寫在該方法的最後,因為就是比較容易理解。
問題:
對於getRequest方法
<code> public Request getRequest() { while (queue.peek() == null) { try { synchronized(this) {} wait(); } } catch (InterruptedException e) { } } return queue.remove(); }/<code>
如果將鎖加在while循環代碼體上,會不會出問題?
答案:
會出問題,原因:如果有兩個線程都同時wait,之後被喚醒,如果此時只有一個請求,那麼queue.remove會出錯,因為一個線程已經取出數據,另一個就沒法取出數據了。
問題:
對於getRequest方法將try/catch放在while循環外部,程序會不會出問題?
答案:會出問題,因為wait方法如果被打斷,那麼就會執行queue.remove方法,此時不會再調用queue.peek方法做見檢查,如果此時queue中沒有請求,就會出錯。
問題:
如果將getRequest方法中的wait改為Thread.sleep方法,程序是否有問題?
答案:會有問題,原因是:sleep方法不會是釋放鎖,wait會釋放實例對象的鎖。由於sleep不釋放鎖,那麼線程將會一直持有鎖,而其他線程就無法放入request,那麼queue.peek就會一直為null,此時程序整個就沒有任何活動,這種情況稱為活鎖。