Java併發(二)異步轉同步

目錄java

  前置條件:構造一個異步調用編程

  1、使用wait和notify方法併發

  2、使用條件鎖dom

  3、Future異步

  4、使用CountDownLatchasync

  5、使用CyclicBarrieride

  總結函數

 

在Java併發編程中,常常會由於須要提升響應速度而將請求異步化,即將同步請求轉化爲異步處理,這是很天然能想到的一種處理方式。相反,在有些場景下也須要將異步處理轉化爲同步的方式。this

首先介紹一下同步調用和異步調用的概念:spa

  同步調用:調用方在調用過程當中,持續等待返回結果。

  異步調用:調用方在調用過程當中,不直接等待返回結果,而是執行其餘任務,結果返回形式一般爲回調函數。

其實,二者的區別仍是很明顯的,這裏也再也不細說,咱們主要來講一下Java如何將異步調用轉爲同步。換句話說,就是須要在異步調用過程當中,持續阻塞至得到調用結果。接下來將介紹5種Java併發編程中異步轉同步的方法。

  1. 使用wait和notify方法
  2. 使用條件鎖
  3. Future
  4. 使用CountDownLatch
  5. 使用CyclicBarrier

前置條件:構造一個異步調用

首先,寫demo須要先寫基礎設施,這裏是須要構造一個異步調用模型。異步調用類:

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class AsyncCall {

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

    private ExecutorService tp = Executors.newSingleThreadExecutor();

    public void call(AbstractBaseDemo demo) {
        new Thread(() -> {
            long res = random.nextInt(10);
            try {
                Thread.sleep(res * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

    public Future<Long> futureCall() {
        return tp.submit(() -> {
            long res = random.nextInt(10);

            Thread.sleep(res * 1000);
            return res;
        });
    }

    public void shutdown() {
        tp.shutdown();
    }
}
咱們主要關心call方法,這個方法接收了一個demo參數,而且開啓了一個線程,在線程中執行具體的任務,並利用demo的callback方法進行回調函數的調用。你們注意到了這裏的返回結果就是一個[0,10)的長整型,而且結果是幾,就讓線程sleep多久——這主要是爲了更好地觀察實驗結果,模擬異步調用過程當中的處理時間。
至於futureCall和shutdown方法,以及線程池tp都是爲了FutureDemo利用Future來實現作準備的。
demo的基類:
public abstract class AbstractBaseDemo {

    protected AsyncCall asyncCall = new AsyncCall();

    public abstract void callback(long response);

    public void call() {
        System.out.println(Thread.currentThread().getName() + "發起調用");
        asyncCall.call(this);
        System.out.println(Thread.currentThread().getName() + "調用返回");
    }
}

AbstractBaseDemo很是簡單,裏面包含一個異步調用類的實例,另外有一個call方法用於發起異步調用,固然還有一個抽象方法callback須要每一個demo去實現的——主要在回調中進行相應的處理來達到異步調用轉同步的目的。

1、使用wait和notify方法

這個方法實際上是利用了鎖機制,直接貼代碼:

public class ObjectWaitLockDemo extends AbstractBaseDemo {

    private final Object lock = new Object();

    @Override
    public void callback(long response) {
        System.out.println(Thread.currentThread().getName() + "獲得結果");
        System.out.println(response);
        System.out.println(Thread.currentThread().getName() + "調用結束");

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

    public static void main(String[] args) {
        ObjectWaitLockDemo demo = new ObjectWaitLockDemo();

        demo.call();

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

        System.out.println(Thread.currentThread().getName() + "主線程內容");
    }
}
能夠看到在發起調用後,主線程利用wait進行阻塞,等待回調中調用notify或者notifyAll方法來進行喚醒。注意,和你們認知的同樣,這裏wait和notify都是須要先得到對象的鎖的。在主線程中最後咱們打印了一個內容,這也是用來驗證明驗結果的,若是沒有wait和notify,主線程內容會緊隨調用內容馬上打印;而像咱們上面的代碼,主線程內容會一直等待回調函數調用結束纔會進行打印。
沒有使用同步操做的狀況下,打印結果:
main發起調用
main調用返回
main主線程內容
Thread-0獲得結果
7
Thread-0調用結束

 而使用了同步操做後:

main發起調用
main調用返回
Thread-0獲得結果
3
Thread-0調用結束
main主線程內容

2、使用條件鎖

和方法一的原理相似:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo extends AbstractBaseDemo {

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

    @Override
    public void callback(long response) {
        System.out.println(Thread.currentThread().getName() + "獲得結果");
        System.out.println(response);
        System.out.println(Thread.currentThread().getName() + "調用結束");

        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();

        demo.call();

        demo.lock.lock();

        try {
            demo.condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            demo.lock.unlock();
        }

        System.out.println(Thread.currentThread().getName() + "主線程內容");
    }
}

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

3、Future

使用Future的方法和以前不太同樣,咱們調用的異步方法也不同。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class FutureDemo {

    private AsyncCall asyncCall = new AsyncCall();

    public Future<Long> call() {
        Future<Long> future = asyncCall.futureCall();

        asyncCall.shutdown();

        return future;
    }

    public static void main(String[] args) {
        FutureDemo demo = new FutureDemo();

        System.out.println(Thread.currentThread().getName() + "發起調用");
        Future<Long> future = demo.call();
        System.out.println(Thread.currentThread().getName() + "返回結果");

        while (!future.isDone() && !future.isCancelled());

        try {
            System.out.println(future.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "主線程內容");
    }
}
咱們調用futureCall方法,方法中會向線程池tp提交一個Callable,而後返回一個Future,這個Future就是咱們FutureDemo中call中獲得的,獲得future對象以後就能夠關閉線程池啦,調用asyncCall的shutdown方法。關於關閉線程池這裏有一點須要注意,咱們回過頭來看看asyncCall的shutdown方法:
    public void shutdown() {
        tp.shutdown();
    }
發現只是簡單調用了線程池的shutdown方法,而後咱們說注意的點,這裏最好不要用tp的shutdownNow方法,該方法會試圖去中斷線程中正在執行的任務;也就是說,若是使用該方法,有可能咱們的future所對應的任務將被中斷,沒法獲得執行結果。
而後咱們關注主線程中的內容,主線程的阻塞由咱們本身來實現,經過future的isDone和isCancelled來判斷執行狀態,一直到執行完成或被取消。隨後,咱們打印get到的結果。

4、使用CountDownLatch

使用CountDownLatch或許是平常編程中最多見的一種了,也感受是相對優雅的一種:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo extends AbstractBaseDemo {

    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    @Override
    public void callback(long response) {
        System.out.println(Thread.currentThread().getName() + "獲得結果");
        System.out.println(response);
        System.out.println(Thread.currentThread().getName() + "調用結束");

        countDownLatch.countDown();
    }

    public static void main(String[] args) {
        CountDownLatchDemo demo = new CountDownLatchDemo();

        demo.call();

        try {
            demo.countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "主線程內容");
    }
}
正如你們平時使用的那樣,此處在主線程中利用CountDownLatch的await方法進行阻塞,在回調中利用countDown方法來使得其餘線程await的部分得以繼續運行。
固然,這裏和ObjectWaitLockDemo和ReentrantLockDemo中都同樣,主線程中阻塞的部分,均可以設置一個超時時間,超時後能夠再也不阻塞。

5、使用CyclicBarrier

CyclicBarrier的狀況和CountDownLatch有些相似:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo extends AbstractBaseDemo {

    private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    @Override
    public void callback(long response) {
        System.out.println(Thread.currentThread().getName() + "獲得結果");
        System.out.println(response);
        System.out.println(Thread.currentThread().getName() + "調用結束");

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

    public static void main(String[] args) {

        CyclicBarrierDemo demo = new CyclicBarrierDemo();

        demo.call();

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

        System.out.println(Thread.currentThread().getName() + "主線程內容");

    }
}
你們注意一下,CyclicBarrier和CountDownLatch僅僅只是相似,二者仍是有必定區別的。好比,一個能夠理解爲作加法,等到加到這個數字後一塊兒運行;一個則是減法,減到0繼續運行。一個是能夠重複計數的;另外一個不能夠等等等等。
另外,使用CyclicBarrier的時候要注意兩點。第一點,初始化的時候,參數數字要設爲2,由於異步調用這裏是一個線程,而主線程是一個線程,兩個線程都await的時候才能繼續執行,這也是和CountDownLatch區別的部分。第二點,也是關於初始化參數的數值的,和這裏的demo無關,在平時編程的時候,須要比較當心,若是這個數值設置得很大,比線程池中的線程數都大,那麼就很容易引發死鎖了。

總結

綜上,就是本次須要說的幾種方法了。事實上,全部的方法都是同一個原理,也就是在調用的線程中進行阻塞等待結果,而在回調中函數中進行阻塞狀態的解除。

參考:5種必會的Java異步調用轉同步的方法你會幾種

相關文章
相關標籤/搜索