5種SpringMvc的異步處理方式你都瞭解嗎?

引言

說到異步你們確定首先會先想到同步。咱們先來看看什麼是同步?
所謂同步,就是發出一個功能調用時,在沒有獲得結果以前,該調用就不返回或繼續執行後續操做。
簡單來講,同步就是必須一件一件事作,等前一件作完了才能作下一件事。
異步:異步就相反,調用在發出以後,這個調用就直接返回了,不須要等結果。html

瀏覽器同步

瀏覽器發起一個request而後會一直待一個響應response,在這期間裏面它是阻塞的。好比早期咱們在咱們在逛電商平臺的時候買東西咱們打開一個商品的頁面,大體流程是否是多是這樣,每次打開一個頁面都是由一個線程從頭至尾來處理,這個請求須要進行數據庫的訪問須要把商品價格庫存啥的返回頁面,還須要去調用第三方接口,好比優惠券接口等咱們只有等到這些都處理完成後這個線程纔會把結果響應給瀏覽器,在這等結果期間這個線程只能一直在乾等着啥事情也不能幹。這樣的話是否是會有有必定的性能問題。大體的流程以下:
在這裏插入圖片描述前端

瀏覽器異步

爲了解決上面同步阻塞的問題,再Servlet3.0發佈後,提供了一個新特性:異步處理請求。好比咱們仍是進入商品詳情頁面,這時候這個前端發起一個請求,而後會有一個線程來執行這個請求,這個請求須要去數據庫查詢庫存、調用第三方接口查詢優惠券等。這時候這個線程就不用幹等着呢。它的任務到這就完成了,又能夠執行下一個任務了。等查詢數據庫和第三方接口查詢優惠券有結果了,這時候會有一個新的線程來把處理結果返回給前端。這樣的話線程的工做量是不超級飽和,須要不停的幹活,連休息的機會都不給了。java

在這裏插入圖片描述

  • 這個異步是純後端的異步,對前端是無感的,異步也並不會帶來響應時間上的優化,原來該執行多久照樣仍是須要執行多久。可是咱們的請求線程(Tomcat 線程)爲異步servlet以後,咱們能夠當即返回,依賴於業務的任務用業務線程來執行,也就是說,Tomcat的線程能夠當即回收,默認狀況下,Tomcat的核心線程是10,最大線程數是200,咱們能及時回收線程,也就意味着咱們能處理更多的請求,可以增長咱們的吞吐量,這也是異步Servlet的主要做用。

下面咱們就來看看Spring mvc 的幾種異步方式吧
https://docs.spring.io/spring...
在這裏插入圖片描述
在這個以前咱們仍是先簡單的回顧下Servlet 3.1的異步:web

  • 客戶端(瀏覽器、app)發送一個請求
  • Servlet容器分配一個線程來處理容器中的一個servlet
  • servlet調用request.startAsync()開啓異步模式,保存AsyncContext, 而後返回。
  • 這個servlet請求線程以及全部的過濾器均可以結束,但其響應(response)會等待異步線程處理結束後再返回。
  • 其餘線程使用保存的AsyncContext來完成響應
  • 客戶端收到響應

在這裏插入圖片描述

Callable

/**  公衆號:java金融
     * 使用Callable
     * @return
     */
    @GetMapping("callable")
    public Callable<String> callable() {
        System.out.println(LocalDateTime.now().toString() + "--->主線程開始");
        Callable<String> callable = () -> {
            String result = "return callable";
            // 執行業務耗時 5s
            Thread.sleep(5000);
            System.out.println(LocalDateTime.now().toString() + "--->子任務線程("+Thread.currentThread().getName()+")");
            return result;
        };
        System.out.println(LocalDateTime.now().toString() + "--->主線程結束");
        return callable;
    }
       public static String doBusiness() {
        // 執行業務耗時 10s
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return UUID.randomUUID().toString();
    }
  • 控制器先返回一個Callable對象
  • Spring MVC開始進行異步處理,並把該Callable對象提交給另外一個獨立線程的執行器TaskExecutor處理
  • DispatcherServlet和全部過濾器都退出Servlet容器線程,但此時方法的響應對象仍未返回
  • Callable對象最終產生一個返回結果,此時Spring MVC會從新把請求分派回Servlet容器,恢復處理
  • DispatcherServlet再次被調用,恢復對Callable異步處理所返回結果的處理

上面就是Callable的一個執行流程,下面咱們來簡單的分析下源碼,看看是怎麼實現的:
咱們知道SpringMvc是能夠返回json格式數據、或者返回視圖頁面(html、jsp)等,SpringMvc是怎麼實現這個的呢?最主要的一個核心類就是org.springframework.web.method.support.HandlerMethodReturnValueHandler 咱們來看看這個類,這個類就是一個接口,總共就兩個方法;spring

boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

上面這個咱們的請求是返回Callable<String> 這樣一個結果的,咱們會根據這個返回的類型去找全部實現了HandlerMethodReturnValueHandler 這個接口的實現類,最終咱們會根據返回類型經過supportsReturnType這個實現的方法找到一個對應的HandlerMethodReturnValueHandler 實現類,咱們根據返回類型是Callable而後就找到了實現類CallableMethodReturnValueHandler。在這裏插入圖片描述
開啓異步線程的話也就是在handleReturnValue這個方法裏面了,感興趣的你們能夠動手去debug下仍是比較好調試的。數據庫

CompletableFuture 和ListenableFuture

@GetMapping("completableFuture")
    public CompletableFuture<String> completableFuture() {
        // 線程池通常不會放在這裏,會使用static聲明,這只是演示
        ExecutorService executor = Executors.newCachedThreadPool();
        System.out.println(LocalDateTime.now().toString() + "--->主線程開始");
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(IndexController::doBusiness, executor);
        System.out.println(LocalDateTime.now().toString() + "--->主線程結束");
        return completableFuture;
    }

    @GetMapping("listenableFuture")
    public ListenableFuture<String> listenableFuture() {
        // 線程池通常不會放在這裏,會使用static聲明,這只是演示
        ExecutorService executor = Executors.newCachedThreadPool();
        System.out.println(LocalDateTime.now().toString() + "--->主線程開始");
        ListenableFutureTask<String> listenableFuture = new ListenableFutureTask<>(()->   doBusiness());
        executor.execute(listenableFuture);
        System.out.println(LocalDateTime.now().toString() + "--->主線程結束");
        return listenableFuture;
    }

注:這種方式記得不要使用內置的不要使用內置的 ForkJoinPool線程池,須要本身建立線程池不然會有性能問題編程

WebAsyncTask

@GetMapping("asynctask")
    public WebAsyncTask asyncTask() {
        SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
        System.out.println(LocalDateTime.now().toString() + "--->主線程開始");
        WebAsyncTask<String> task = new WebAsyncTask(1000L, executor, ()-> doBusiness());
        task.onCompletion(()->{
            System.out.println(LocalDateTime.now().toString() + "--->調用完成");
        });
        task.onTimeout(()->{
            System.out.println("onTimeout");
            return "onTimeout";
        });
        System.out.println(LocalDateTime.now().toString() + "--->主線程結束");
        return task;
    }

DeferredResult

@GetMapping("deferredResult")
    public DeferredResult<String> deferredResult() {
        System.out.println(LocalDateTime.now().toString() + "--->主線程("+Thread.currentThread().getName()+")開始");
        DeferredResult<String> deferredResult = new DeferredResult<>();
        CompletableFuture.supplyAsync(()-> doBusiness(), Executors.newFixedThreadPool(5)).whenCompleteAsync((result, throwable)->{
            if (throwable!=null) {
                deferredResult.setErrorResult(throwable.getMessage());
            }else {
                deferredResult.setResult(result);
            }
        });
        // 異步請求超時時調用
        deferredResult.onTimeout(()->{
            System.out.println(LocalDateTime.now().toString() + "--->onTimeout");
        });
        // 異步請求完成後調用
        deferredResult.onCompletion(()->{
            System.out.println(LocalDateTime.now().toString() + "--->onCompletion");
        });
        System.out.println(LocalDateTime.now().toString() + "--->主線程("+Thread.currentThread().getName()+")結束");
        return deferredResult;
    }
  • 上面這幾種異步方式都是會等到業務doBusiness執行完以後(10s)纔會把response給到前端,執行請求的主線程會當即結束,響應結果會交給另外的線程來返回給前端。
  • 這種異步跟下面的這個所謂的假異步是不一樣的,這種狀況是由主線程執行完成以後立馬返回值(主線程)給前端,不會等個5s在返回給前端。
@GetMapping("call")
    public String call() {
       new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   Thread.sleep(5000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }).start();
        return "這是個假異步";
    }

這幾種異步方式都跟返回Callable 差很少,都有對應的HandlerMethodReturnValueHandler 實現類,無非就是豐富了本身一些特殊的api、好比超時設置啥的,以及線程池的建立是誰來建立,執行流程基本都是同樣的。json

總結

  • 瞭解spring mvc 的異步編程,對咱們後續學習響應式編程、rxjava、webflux等都是有好處的。
  • 異步編程能夠幫咱們高效的利用系統資源。

結束

  • 因爲本身才疏學淺,不免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。

在這裏插入圖片描述

站在巨人的肩膀上摘蘋果:
https://blog.csdn.net/f641385...後端

相關文章
相關標籤/搜索