併發程式設計 —— 自己寫一個非同步回撥 API

莫那·魯道發表於2018-04-30

1. 前言

在併發程式設計中,非同步回撥的效率不言而喻,在業務開發中,如果由阻塞的任務需要執行,必然要使用非同步執行緒。並且,如果我們想在非同步執行之後,根據他的結果執行一些動作。

JDK 8 之前的 Future 只能解決上面需求的一半問題,即非同步執行,返回一個 Future,需要程式設計師呼叫 get 方法等待,或者使用 isDone 輪詢。

效率不高。

JDK 8 新出的 CompletableFuture API 可以解決這個問題。但他的 API, 說實話,不太好用。

我們只想要一個簡單的 API,能實現我們的回撥功能。

我需要 3 個功能:

  1. 能通過 get 之類的方法返回結果。
  2. 能設定監聽器進行回撥。
  3. 可以在業務執行緒中設定成功或者失敗。

樓主寫一個簡單的例子,借鑑了 Netty 的非同步 API,希望能起到拋磚引玉的作用。

2. 設計

根據我們的需求: 第一,我們需要一個類,擁有 get 方法和 addListener 方法。 第二,我們需要一個類,能夠回撥我們設定的監聽器。 第三,我們需要一個類,能夠在業務執行緒中設定成功或者失敗。

3. 初步實現

設計一個監聽器介面:

/**
 * 監聽器
 * @author stateis0 
 */
public interface MyListener {
  /**
   * 子類需要重寫此方法,在非同步任務完成之後會回撥此方法。
   * @param promise 非同步結果佔位符。
   */
  void operationComplete(MyPromise promise);
}
複製程式碼

設計一個非同步佔位符,類似 Future:

/**
 * 非同步執行結果佔位符
 *
 * @author stateis0
 */
public class MyPromise {

  /** 監聽器集合*/
  List<MyListener> listeners = new ArrayList<MyListener>();

  /** 是否成功*/
  boolean success;

  /** 執行結果**/
  Object result;

  /** 設定事變計數器**/
  int failCount;

  /**
   * 設定成功,並通知所有監聽器。
   * @param result 結果
   * @return 是否成功
   */
  public boolean setSuccess(Object result) {
    if (success) {
      return false;
    }

    success = true;
    this.result = result;

    signalListeners();
    return true;
  }

  /**
   * 通知所有監聽器,回撥監聽器方法。
   */
  private void signalListeners() {
    for (MyListener l : listeners) {
      l.operationComplete(this);
    }
  }

  /**
   * 設定失敗
   * @param e 異常物件
   * @return 設定是否成功
   */
  public boolean setFail(Exception e) {
    if (failCount > 0) {
      return false;
    }
    ++failCount;
    result = e;
    signalListeners();
    return true;
  }

  /**
   * 是否成功執行
   */
  public boolean isSuccess() {
    return success;
  }

  /**
   * 新增監聽器
   * @param myListener 監聽器
   */
  public void addListener(MyListener myListener) {
    listeners.add(myListener);
  }

  /**
   * 刪除監聽器
   * @param myListener 監聽器
   */
  public void removeListener(MyListener myListener) {
    listeners.remove(myListener);
  }

  /**
   * 獲取執行結果
   */
  public Object get() {
    return result;
  }
}
複製程式碼

我們希望使用執行緒池執行此類任務,所以需要一個自定義的 Runnable,而在這個 Runnable 中,我們需要做一些簡單的手腳:

/**
 * 一個任務類,通過重寫 doWork 方法執行任務
 * @param <V> 返回值型別
 * @author stateis0 
 */
public abstract class MyRunnable<V> implements Runnable {

  final MyPromise myPromise;

  protected MyRunnable(MyPromise myPromise) {
    this.myPromise = myPromise;
  }

  @Override
  public void run() {
    try {
      V v = doWork();
      myPromise.setSuccess(v);
    } catch (Exception e) {
      myPromise.setFail(e);
    }
  }

  /**
   * 子類需要重寫此方法。並返回值,這個值由 Promise 的 get 方法返回。
   */
  public abstract V doWork();
}
複製程式碼

4. 寫個 Demo 測試一下

/**
 * @author stateis0
 */
public class MyDemo {

  public static void main(String[] args) {

    // 佔位物件
    final MyPromise myPromise = new MyPromise();

    final Dto dto = new Dto();

    // 執行緒池
    Executor executor = Executors.newFixedThreadPool(1);

    // 非同步執行任務,
    executor.execute(new MyRunnable<String>(myPromise) {
      @Override
      public String doWork() {
        return dto.doSomething();
      }
    });

    // 新增一個監聽器
    myPromise.addListener(new MyListener() {
      // 當任務完成後,就執行此方法。
      @Override
      public void operationComplete(MyPromise promise) {
        // 獲取結果
        String result;
        // 如果任務成功執行了
        if (promise.isSuccess()) {
          // 獲取結果並列印
          result = (String) promise.get();
          System.out.println("operationComplete ----> " + result);
        }
        // 如果失敗了, 列印異常堆疊
        else {
          ((Exception) promise.get()).printStackTrace();
        }
      }
    });
  }

}

class Dto {

  public String doSomething() {
    System.out.println("doSomething");
//    throw new RuntimeException("cxs");
    return "result is success";
  }
}
複製程式碼

執行結果:

doSomething
operationComplete ----> result is success
複製程式碼

符合我們的預期。我們希望在業務物件 Dto 的 doSomething 成功返回之後,回撥監聽器的 operationComplete 方法。如果失敗,列印異常堆疊。

當然,整體程式碼比較簡單,僅僅只是拋磚引玉。

實際上,如果直接向 Callable 或者 Runnable 傳入一個業務物件,當 call 方法或者 run 方法執行完畢,就可以根據執行結果執行我們的業務物件的方法了。這樣就是一個最簡單直接的非同步回撥。

只是這樣過於耦合。

非同步任務和業務的任務耦合在了一起,並且不能新增多個監聽器,也無法使用 promise 的 setSuccess 功能和 setFail 功能,這兩個功能可以在業務執行緒中設定成功或者失敗,靈活性更高。

關於非同步,我們可以多看看 Netty 的 API 設計,易懂好用。

相關文章