話題:Guarded Suspension模式:等待喚醒機制的規範實現
前不久,同事小灰工作中遇到一個問題,他開發了一個Web項目:Web版的文件瀏覽器,通過它用戶可以在瀏覽器裏查看服務器上的目錄和文件。這個項目依賴運維部門提供的文件瀏覽服務,而這個文件瀏覽服務只支持消息隊列(MQ)方式接入。消息隊列在互聯網大廠中用的非常多,主要用作流量削峯和系統解耦。在這種接入方式中,發送消息和消費結果這兩個操作之間是異步的,你可以參考下面的示意圖來理解。
在小灰的這個Web項目中,用戶通過瀏覽器發過來一個請求,會被轉換成一個異步消息發送給MQ,等MQ返回結果後,再將這個結果返回至瀏覽器。小灰同學的問題是:給MQ發送消息的線程是處理Web請求的線程T1,但消費MQ結果的線程並不是線程T1,那線程T1如何等待MQ的返回結果呢?爲了便於你理解這個場景,將其代碼化了,示例代碼如下。
class Message{
String id;
String content;
}
//該方法可以發送消息
void send(Message msg){
//省略相關代碼
}
//MQ消息返回後會調用該方法
//該方法的執行線程不同於發送消息的線程
void onMessage(Message msg){
//省略相關代碼
}
//處理瀏覽器發來的請求
Respond handleWebReq(){
//創建一消息
Message msg1 = new Message("1","{...}");
//發送消息
send(msg1);
//如何等待MQ返回的消息呢?傻傻的等待還機智的等待呢??
String result = ...;
}
類似於異步轉同步問題,今天咱們再仔細聊聊這個問題,讓你知其所以然,遇到類似問題也能自己設計出方案來。
Guarded Suspension模式
上面小灰遇到的問題,在現實世界裏比比皆是,只是我們一不小心就忽略了。比如,項目組團建要外出聚餐,我們提前預訂了一個包間,然後興沖沖地奔過去,到那兒後大堂經理看了一眼包間,發現服務員正在收拾,就會告訴我們:“您預訂的包間服務員正在收拾,請您稍等片刻。”過了一會,大堂經理髮現包間已經收拾完了,於是馬上帶我們去包間就餐。
我們等待包間收拾完的這個過程和小灰遇到的等待MQ返回消息本質上是一樣的,都是等待一個條件滿足:就餐需要等待包間收拾完,小灰的程序裏要等待MQ返回消息。
那我們來看看現實世界裏是如何解決這類問題的呢?現實世界裏大堂經理這個角色很重要,我們是否等待,完全是由他來協調的。通過類比,相信你也一定有思路了:我們的程序裏,也需要這樣一個大堂經理。的確是這樣,那程序世界裏的大堂經理該如何設計呢?其實設計方案前人早就搞定了,而且還將其總結成了一個設計模式:Guarded Suspension。所謂Guarded Suspension,直譯過來就是“保護性地暫停”。那下面我們就來看看,Guarded Suspension模式是如何模擬大堂經理進行保護性地暫停的。
下圖就是Guarded Suspension模式的結構圖,非常簡單,一個對象GuardedObject,內部有一個成員變量——受保護的對象,以及兩個成員方法——get(Predicate<T> p)
和onChanged(T obj)
方法。其中,對象GuardedObject就是我們前面提到的大堂經理,受保護對象就是餐廳裏面的包間;受保護對象的get()方法對應的是我們的就餐,就餐的前提條件是包間已經收拾好了,參數p就是用來描述這個前提條件的;受保護對象的onChanged()方法對應的是服務員把包間收拾好了,通過onChanged()方法可以fire一個事件,而這個事件往往能改變前提條件p的計算結果。下圖中,左側的綠色線程就是需要就餐的顧客,而右側的藍色線程就是收拾包間的服務員。
GuardedObject的內部實現非常簡單,是管程的一個經典用法,你可以參考下面的示例代碼,核心是:get()方法通過條件變量的await()方法實現等待,onChanged()方法通過條件變量的signalAll()方法實現喚醒功能。邏輯還是很簡單的,所以這裏就不再詳細介紹了。
class GuardedObject<T>{
//受保護的對象
T obj;
final Lock lock = new ReentrantLock();
final Condition done = lock.newCondition();
final int timeout=1;
//獲取受保護對象
T get(Predicate<T> p) {
lock.lock();
try {
//MESA管程推薦寫法
while(!p.test(obj)){
done.await(timeout, TimeUnit.SECONDS);
}
}catch(InterruptedException e){
throw new RuntimeException(e);
}finally{
lock.unlock();
}
//返回非空的受保護對象
return obj;
}
//事件通知方法
void onChanged(T obj) {
lock.lock();
try {
this.obj = obj;
done.signalAll();
} finally {
lock.unlock();
}
}
}
擴展Guarded Suspension模式
上面我們介紹了Guarded Suspension模式及其實現,這個模式能夠模擬現實世界裏大堂經理的角色,那現在我們再來看看這個“大堂經理”能否解決小灰同學遇到的問題。
Guarded Suspension模式裏GuardedObject有兩個核心方法,一個是get()方法,一個是onChanged()方法。很顯然,在處理Web請求的方法handleWebReq()中,可以調用GuardedObject的get()方法來實現等待;在MQ消息的消費方法onMessage()中,可以調用GuardedObject的onChanged()方法來實現喚醒。
//處理瀏覽器發來的請求
Respond handleWebReq(){
//創建一消息
Message msg1 = new Message("1","{...}");
//發送消息
send(msg1);
//利用GuardedObject實現等待
GuardedObject<Message> go =new GuardObjec<>();
Message r = go.get(t->t != null);
}
void onMessage(Message msg){
//如何找到匹配的go?
GuardedObject<Message> go=???
go.onChanged(msg);
}
但是在實現的時候會遇到一個問題,handleWebReq()裏面創建了GuardedObject對象的實例go,並調用其get()方等待結果,那在onMessage()方法中,如何才能夠找到匹配的GuardedObject對象呢?這個過程類似服務員告訴大堂經理某某包間已經收拾好了,大堂經理如何根據包間找到就餐的人。現實世界裏,大堂經理的頭腦中,有包間和就餐人之間的關係圖,所以服務員說完之後大堂經理立刻就能把就餐人找出來。
我們可以參考大堂經理識別就餐人的辦法,來擴展一下Guarded Suspension模式,從而使它能夠很方便地解決小灰同學的問題。在小灰的程序中,每個發送到MQ的消息,都有一個唯一性的屬性id,所以我們可以維護一個MQ消息id和GuardedObject對象實例的關係,這個關係可以類比大堂經理大腦裏維護的包間和就餐人的關係。
有了這個關係,我們來看看具體如何實現。下面的示例代碼是擴展Guarded Suspension模式的實現,擴展後的GuardedObject內部維護了一個Map,其Key是MQ消息id,而Value是GuardedObject對象實例,同時增加了靜態方法create()和fireEvent();create()方法用來創建一個GuardedObject對象實例,並根據key值將其加入到Map中,而fireEvent()方法則是模擬的大堂經理根據包間找就餐人的邏輯。
class GuardedObject<T>{
//受保護的對象
T obj;
final Lock lock = new ReentrantLock();
final Condition done =lock.newCondition();
final int timeout=2;
//保存所有GuardedObject
final static Map<Object, GuardedObject> gos=new ConcurrentHashMap<>();
//靜態方法創建GuardedObject
static <K> GuardedObject create(K key){
GuardedObject go = new GuardedObject();
gos.put(key, go);
return go;
}
static <K, T> void fireEvent(K key, T obj){
GuardedObject go=gos.remove(key);
if (go != null){
go.onChanged(obj);
}
}
//獲取受保護對象
T get(Predicate<T> p) {
lock.lock();
try {
//MESA管程推薦寫法
while(!p.test(obj)){
done.await(timeout, TimeUnit.SECONDS);
}
}catch(InterruptedException e){
throw new RuntimeException(e);
}finally{
lock.unlock();
}
//返回非空的受保護對象
return obj;
}
//事件通知方法
void onChanged(T obj) {
lock.lock();
try {
this.obj = obj;
done.signalAll();
} finally {
lock.unlock();
}
}
}
這樣利用擴展後的GuardedObject來解決小灰同學的問題就很簡單了,具體代碼如下所示。
//處理瀏覽器發來的請求
Respond handleWebReq(){
int id=序號生成器.get();
//創建一消息
Message msg1 = new Message(id,"{...}");
//創建GuardedObject實例
GuardedObject<Message> go= GuardedObject.create(id);
//發送消息
send(msg1);
//等待MQ消息
Message r = go.get(t->t != null);
}
void onMessage(Message msg){
//喚醒等待的線程
GuardedObject.fireEvent(msg.id, msg);
}
總結
Guarded Suspension模式本質上是一種等待喚醒機制的實現,只不過Guarded Suspension模式將其規範化了。規範化的好處是你無需重頭思考如何實現,也無需擔心實現程序的可理解性問題,同時也能避免一不小心寫出個Bug來。但Guarded Suspension模式在解決實際問題的時候,往往還是需要擴展的,擴展的方式有很多,本篇文章就直接對GuardedObject的功能進行了增強,Dubbo中DefaultFuture這個類也是採用的這種方式,你可以對比着來看,相信對DefaultFuture的實現原理會理解得更透徹。當然,你也可以創建新的類來實現對Guarded Suspension模式的擴展。
Guarded Suspension模式也常被稱作Guarded Wait模式、Spin Lock模式(因爲使用了while循環去等待),這些名字都很形象,不過它還有一個更形象的非官方名字:多線程版本的if。單線程場景中,if語句是不需要等待的,因爲在只有一個線程的條件下,如果這個線程被阻塞,那就沒有其他活動線程了,這意味着if判斷條件的結果也不會發生變化了。但是多線程場景中,等待就變得有意義了,這種場景下,if判斷條件的結果是可能發生變化的。所以,用“多線程版本的if”來理解這個模式會更簡單。
Demo
1
public class SuspensionClient {
public static void main(String[] args) throws InterruptedException {
final RequestQueue queue = new RequestQueue();
new ClientThread(queue, "Alex").start();
ServerThread serverThread = new ServerThread(queue);
serverThread.start();
//serverThread.join();
Thread.sleep(10000);
serverThread.close();
}
}
2
public class RequestQueue {
private final LinkedList<Request> queue = new LinkedList<>();
public Request getRequest() {
synchronized (queue) {
while (queue.size() <= 0) {
try {
queue.wait();
} catch (InterruptedException e) {
return null;
}
}
Request request = queue.removeFirst();
return request;
}
}
public void putRequest(Request request) {
synchronized (queue) {
queue.addLast(request);
queue.notifyAll();
}
}
}
3
public class ClientThread extends Thread {
private final RequestQueue queue;
private final Random random;
private final String sendValue;
public ClientThread(RequestQueue queue, String sendValue) {
this.queue = queue;
this.sendValue = sendValue;
random = new Random(System.currentTimeMillis());
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Client -> request " + sendValue);
//put就會喚醒等待的
queue.putRequest(new Request(sendValue));
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4
public class ServerThread extends Thread {
private final RequestQueue queue;
private final Random random;
private volatile boolean closed = false;
public ServerThread(RequestQueue queue) {
this.queue = queue;
random = new Random(System.currentTimeMillis());
}
@Override
public void run() {
while (!closed) {
Request request = queue.getRequest();
if (null == request) {
System.out.println("Received the empty request.");
continue;
}
System.out.println("Server ->" + request.getValue());
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
return;
}
}
}
/**
* 這個方法作用是:巧妙的關閉了等待從RequestQueue獲取Request對象的線程
* 類似於線程這樣重資源的使用需要注意要釋放資源
*/
public void close() {
this.closed = true;
this.interrupt();//queue.wait();這裏可能一直阻塞了 this是當前線程
}
}
5
public class Request {
final private String value;
public Request(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}