Java 多線程模式 —— Guarded Suspension 模式 Guarded Suspension 模式的介紹 Guarded Suspension 模式的使用 總結

Guarded Suspension 模式的介紹

我們只從字面上看,Guarded Suspension 是受保護暫停的意思。

Guarded Suspension 模式

在實際的併發編程中,Guarded Suspension 模式適用於某個線程需要滿足特定的條件(Predicate)才能執行某項任務(訪問受保護對象)。條件未滿足時,則掛起線程,讓線程一直處於 WAITING 狀態,直到條件滿足後該線程纔可以執行任務。有點類似於 Java 的 wait() 和 notify() 方法。

Guarded Suspension 模式通過讓線程的等待,來保證受保護對象的安全。

Guarded Suspension 模式是等待喚醒機制的一種規範實現,又被稱爲 Guarded Wait 模式、Spin Lock 模式、多線程版本的 if。

應用場景

Guarded Suspension 模式是多線程編程基礎的設計模式,適用於很多應用場景,也可以和其他的多線程模式組合使用。

下面列舉兩個場景。

場景一

在工業自動化場景下,某個自動化流水線上使用工業相機通過圖像算法識別上料臺是否有物品,當算法識別到上料臺有物品時,機械臂纔會去抓取物品,否則機械臂一直處於等待狀態。圖像算法的調用和機械臂的控制,分別處於不同的線程。對於這樣的多線程協作,正好可以使用 Guarded Suspension 模式。

場景二

Dubbo 的調用是異步的,卻可以得到同步的返回結果。這也是經典的異步轉同步的方法。
翻閱 Dubbo 的 DefaultFeture 類,我們可以看到它的源碼也使用了 Guarded Suspension 模式。

Guarded Suspension 模式的使用

通用的 Guarded Suspension 模式

基於上述的說明,我們創建一個通用的受保護對象 GuardedObject,這裏用到了 Lock、Condition 來實現該對象。當然,用 Java 的 wait() 和 notify()/notifyAll() 方法也是可以的。

public class GuardedObject<K,V> {

    private static final int TIMEOUT = 10;
    private static final Map<Object, GuardedObject> goMap = new ConcurrentHashMap<>();

    // 受保護對象
    private V obj;
    private final Lock lock = new ReentrantLock();
    private final Condition done = lock.newCondition();

    public static <K,V> GuardedObject create(K key) {
        GuardedObject go = new GuardedObject<K,V>();
        goMap.put(key, go);
        return go;
    }

    // 喚醒等待的線程
    public static <K, V> void fireEvent(K key, V obj) {
        GuardedObject go = goMap.remove(key);
        if (go != null) {
            go.onChange(obj);
        }
    }

    // 獲取受保護對象
    public V get(Predicate<V> p) throws WatcherException{
        lock.lock();
        try {
            while (!p.test(obj)) {
                done.await(TIMEOUT, TimeUnit.SECONDS);
            }
        } catch (InterruptedException e) {
            throw new WatcherException(e);
        } finally {
            lock.unlock();
        }
        return obj;
    }

    // 事件通知方法
    public void onChange(V obj) {
        lock.lock();
        try {
            this.obj = obj;
            done.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

在 GuardedObject 對象中, goMap 是靜態變量,所以這個 Map 存儲了所有的 GuardedObject 對象。

另外,WatcherException 是我們業務異常,在這裏可以替換成 RuntimeException。

我們用代碼來模擬 Guarded Suspension 模式的使用過程。

class SomeObj{

}

public class Test {

    public static void main(String[] args) {

        try {
            GuardedObject<Integer,SomeObj> guardedObject = GuardedObject.create(1);

            Thread t1 = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "線程開始訪問受保護對象");
                guardedObject.get(e -> {
                    return e!=null;
                });
                System.out.println(Thread.currentThread().getName() + "線程訪問到了受保護對象");
            });
            t1.setName("t1");
            t1.start();

            Thread t2 = new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "線程開始做準備的工作");
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                GuardedObject.fireEvent(1,new SomeObj());
                System.out.println(Thread.currentThread().getName() + "線程準備工作完成,條件滿足,喚醒等待的線程");
            });
            t2.setName("t2");
            t2.start();

            t1.join();
            t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果:

t1線程開始訪問受保護對象
t2線程開始做準備的工作
t2線程準備工作完成,條件滿足,喚醒等待的線程
t1線程訪問到了受保護對象

支持超時機制的 Guarded Suspension 模式

對於將異步調用轉化成同步調用的操作時,肯定是需要超時機制的。因此,將上述代碼修改一下,增加了超時機制。

public class GuardedObject<K,V> {

    private static final int TIMEOUT = 10;
    private static final Map<Object, GuardedObject> goMap = new ConcurrentHashMap<>();

    // 受保護對象
    private V obj;
    private final Lock lock = new ReentrantLock();
    private final Condition done = lock.newCondition();

    public static <K,V> GuardedObject create(K key) {
        GuardedObject go = new GuardedObject<K,V>();
        goMap.put(key, go);
        return go;
    }

    /**
     * 判斷條件
     * @return
     */
    private boolean isArrived() {
        return obj != null;
    }

    /**
     * 喚醒等待的線程
     * @param key
     * @param obj
     */
    public static <K, V> void fireEvent(K key, V obj) {
        GuardedObject go = goMap.remove(key);
        if (go != null) {
            go.onChange(obj);
        }
    }

    /**
     * 獲取受保護對象
     * @return
     * @throws WatcherException
     */
    public V get() throws WatcherException{
        return get(TIMEOUT);
    }

    /**
     * 獲取受保護對象
     * @return
     * @throws WatcherException
     */
    public V get(long timeout) throws WatcherException{
        lock.lock();
        try {
            while (!isArrived()) {
                //等待,增加超時機制
                try {
                    done.await(timeout, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    throw new WatcherException(e);
                }

                if (!isArrived()) {
                    throw new WatcherException("timeout");
                }
            }
        } finally {
            lock.unlock();
        }
        return obj;
    }

    /**
     * 事件通知方法
     * @param obj
     */
    private void onChange(V obj) {
        lock.lock();
        try {
            this.obj = obj;
            done.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

如果超時的話,GuardedObject#get() 會拋出異常,在調用時捕獲異常就可以了。

總結

筆者正好使用該模式,將某個串口調用的第三方庫 (https://github.com/NeuronRobotics/nrjavaserial)
從原先只支持異步調用,改成了也可以支持同步調用,增加了超時的機制,並應用於生產環境中。

對於多線程的協作,當然還有其他方式。比如 A 線程輪詢等待 B 線程結束後,再去執行 A 線程的任務。對於這種情況,肯定是使用 Guarded Suspension 模式更佳。或者通過 eventbus 這樣的事件總線來實現多線程的協作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章