Java多線程模式-GuardedSuspension模式

場景

當家庭主婦在廚房炒菜時,門鈴響了,快遞員送來快遞,由於炒菜你不能馬上去開門,而是讓快遞員在門口等一會兒,才去打開門。

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,此時程序整個就沒有任何活動,這種情況稱為活鎖。