1. 程式人生 > 程式設計 >Java非同步呼叫轉同步方法例項詳解

Java非同步呼叫轉同步方法例項詳解

先說一下對非同步和同步的理解:

同步呼叫:呼叫方在呼叫過程中,持續等待返回結果。

非同步呼叫:呼叫方在呼叫過程中,不直接等待返回結果,而是執行其他任務,結果返回形式通常為回撥函式。

其實,兩者的區別還是很明顯的,這裡也不再細說,我們主要來說一下Java如何將非同步呼叫轉為同步。換句話說,就是需要在非同步

呼叫過程中,持續阻塞至獲得呼叫結果。

不賣關子,先列出五種方法,然後一一舉例說明:

  • 使用wait和notify方法
  • 使用條件鎖
  • Future
  • 使用CountDownLatch
  • 使用CyclicBarrier

0.構造一個非同步呼叫

首先,寫demo需要先寫基礎設施,這裡的話主要是需要構造一個非同步呼叫模型。非同步呼叫類:

public class AsyncCall {

  private Random random = new Random(System.currentTimeMillis());

  private ExecutorService tp = Executors.newSingleThreadExecutor();

  //demo1,2,4,5呼叫方法
  public void call(BaseDemo demo){

    new Thread(()->{
      long res = random.nextInt(10);

      try {
        Thread.sleep(res*1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }

      demo.callback(res);
    }).start();

  }
  //demo3呼叫方法
  public Future<Long> futureCall(){
    return tp.submit(()-> {
      long res = random.nextInt(10);
      try {
        Thread.sleep(res*1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      return res;
    });
  }
  public void shutdown(){
    tp.shutdown();
  }
}

我們主要關心call方法,這個方法接收了一個demo引數,並且開啟了一個執行緒,線上程中執行具體的任務,並利用demo的callback方法進行回撥函式的呼叫。大家注意到了這裡的返回結果就是一個[0,10)的長整型,並且結果是幾,就讓執行緒sleep多久——這主要是為了更好地觀察實驗結果,模擬非同步呼叫過程中的處理時間。
至於futureCall和shutdown方法,以及執行緒池tp都是為了demo3利用Future來實現做準備的。

demo的基類:

public abstract class BaseDemo {
  protected AsyncCall asyncCall = new AsyncCall();
  public abstract void callback(long response);
  public void call(){
    System.out.println("發起呼叫");
    asyncCall.call(this);
    System.out.println("呼叫返回");
  }
}

BaseDemo非常簡單,裡面包含一個非同步呼叫類的例項,另外有一個call方法用於發起非同步呼叫,當然還有一個抽象方法callback需要每個demo去實現的——主要在回撥中進行相應的處理來達到非同步呼叫轉同步的目的。

1. 使用wait和notify方法

這個方法其實是利用了鎖機制,直接貼程式碼:

public class Demo1 extends BaseDemo{

  private final Object lock = new Object();

  @Override
  public void callback(long response) {
    System.out.println("得到結果");
    System.out.println(response);
    System.out.println("呼叫結束");

    synchronized (lock) {
      lock.notifyAll();
    }

  }

  public static void main(String[] args) {

    Demo1 demo1 = new Demo1();

    demo1.call();

    synchronized (demo1.lock){
      try {
        demo1.lock.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    System.out.println("主執行緒內容");
  }
}

可以看到在發起呼叫後,主執行緒利用wait進行阻塞,等待回撥中呼叫notify或者notifyAll方法來進行喚醒。注意,和大家認知的一樣,這裡wait和notify都是需要先獲得物件的鎖的。在主執行緒中最後我們列印了一個內容,這也是用來驗證實驗結果的,如果沒有wait和notify,主執行緒內容會緊隨呼叫內容立刻列印;而像我們上面的程式碼,主執行緒內容會一直等待回撥函式呼叫結束才會進行列印。

沒有使用同步操作的情況下,列印結果:

發起呼叫
呼叫返回
主執行緒內容
得到結果
1
呼叫結束

而使用了同步操作後:

發起呼叫
呼叫返回
得到結果
9
呼叫結束
主執行緒內容

2. 使用條件鎖

和方法一的原理類似:

public class Demo2 extends BaseDemo {

  private final Lock lock = new ReentrantLock();
  private final Condition con = lock.newCondition();

  @Override
  public void callback(long response) {

    System.out.println("得到結果");
    System.out.println(response);
    System.out.println("呼叫結束");
    lock.lock();
    try {
      con.signal();
    }finally {
      lock.unlock();
    }

  }

  public static void main(String[] args) {

    Demo2 demo2 = new Demo2();

    demo2.call();

    demo2.lock.lock();

    try {
      demo2.con.await();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }finally {
      demo2.lock.unlock();
    }
    System.out.println("主執行緒內容");
  }
}

基本上和方法一沒什麼區別,只是這裡使用了條件鎖,兩者的鎖機制有所不同。

3. Future

使用Future的方法和之前不太一樣,我們呼叫的非同步方法也不一樣。

public class Demo3{
  private AsyncCall asyncCall = new AsyncCall();
  public Future<Long> call(){
    Future<Long> future = asyncCall.futureCall();
    asyncCall.shutdown();
    return future;
  }
  public static void main(String[] args) {
    Demo3 demo3 = new Demo3();
    System.out.println("發起呼叫");
    Future<Long> future = demo3.call();
    System.out.println("返回結果");
    while (!future.isDone() && !future.isCancelled());
    try {
      System.out.println(future.get());
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }

    System.out.println("主執行緒內容");

  }
}

我們呼叫futureCall方法,方法中會想執行緒池tp提交一個Callable,然後返回一個Future,這個Future就是我們demo3中call中得到的,得到future物件之後就可以關閉執行緒池啦,呼叫asyncCall的shutdown方法。關於關閉執行緒池這裡有一點需要注意,我們回過頭來看看asyncCall的shutdown方法:

public void shutdown(){
    tp.shutdown();
  }

發現只是簡單呼叫了執行緒池的shutdown方法,然後我們說注意的點,這裡最好不要用tp的shutdownNow方法,該方法會試圖去中斷執行緒中中正在執行的任務;也就是說,如果使用該方法,有可能我們的future所對應的任務將被中斷,無法得到執行結果。
然後我們關注主執行緒中的內容,主執行緒的阻塞由我們自己來實現,通過future的isDone和isCancelled來判斷執行狀態,一直到執行完成或被取消。隨後,我們列印get到的結果。

4. 使用CountDownLatch

使用CountDownLatch或許是日常程式設計中最常見的一種了,也感覺是相對優雅的一種:

public class Demo4 extends BaseDemo{

  private final CountDownLatch countDownLatch = new CountDownLatch(1);

  @Override
  public void callback(long response) {

    System.out.println("得到結果");
    System.out.println(response);
    System.out.println("呼叫結束");

    countDownLatch.countDown();
  }
  public static void main(String[] args) {
    Demo4 demo4 = new Demo4();
    demo4.call();
    try {
      demo4.countDownLatch.await();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("主執行緒內容");

  }
}

正如大家平時使用的那樣,此處在主執行緒中利用CountDownLatch的await方法進行阻塞,在回撥中利用countDown方法來使得其他執行緒await的部分得以繼續執行。

當然,這裡和demo1和demo2中都一樣,主執行緒中阻塞的部分,都可以設定一個超時時間,超時後可以不再阻塞。

5. 使用CyclicBarrier

public class Demo5 extends BaseDemo{

  private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

  @Override
  public void callback(long response) {

    System.out.println("得到結果");
    System.out.println(response);
    System.out.println("呼叫結束");

    try {
      cyclicBarrier.await();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (BrokenBarrierException e) {
      e.printStackTrace();
    }

  }

  public static void main(String[] args) {

    Demo5 demo5 = new Demo5();

    demo5.call();

    try {
      demo5.cyclicBarrier.await();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (BrokenBarrierException e) {
      e.printStackTrace();
    }

    System.out.println("主執行緒內容");

  }
}

大家注意一下,CyclicBarrier和CountDownLatch僅僅只是類似,兩者還是有一定區別的。比如,一個可以理解為做加法,等到加到這個數字後一起執行;一個則是減法,減到0繼續執行。一個是可以重複計數的;另一個不可以等等等等。

另外,使用CyclicBarrier的時候要注意兩點。第一點,初始化的時候,引數數字要設為2,因為非同步呼叫這裡是一個執行緒,而主執行緒是一個執行緒,兩個執行緒都await的時候才能繼續執行,這也是和CountDownLatch區別的部分。第二點,也是關於初始化引數的數值的,和這裡的demo無關,在平時程式設計的時候,需要比較小心,如果這個數值設定得很大,比執行緒池中的執行緒數都大,那麼就很容易引起死鎖了。

總結

綜上,就是本次需要說的幾種方法了。事實上,所有的方法都是同一個原理,也就是在呼叫的執行緒中進行阻塞等待結果,而在回撥中函式中進行阻塞狀態的解除。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。