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


分享到:


相關文章: