05.OkHttp線程池

目錄介紹

  • 01.線程池的理解
  • 02.Dispatcher類詳解
  • 03.OKHttp的任務調度
  • 04.OKHttp調度優雅之處

01.線程池的理解

  • android中的異步任務
    • android的異步任務一般都是用Thread+Handler或者AsyncTask來實現,其中筆者當初經歷過各種各樣坑,特別是內存泄漏,當初筆者可是相當的欲死欲仙啊!所以現在很少有開發者還在用這一套來做異步任務,現在一般都是Rxjava爲主,當然還有自己自定義的異步任務框架(比如筆者),像RxJava都幫我們寫好了對應場景的線程池,這是爲什麼?
  • 線程池的理解
    • 對線程池的理解是有兩個層次,一種是狹隘的,一種是廣義的,那麼咱們各自都說下。
    • 狹義上的線程池:線程池是一種多線程處理形式,處理過程中將任務添加到隊列中,後面再創建線程去處理這些任務,線程池裏面的線程都是後臺線程,每個線程都是默認的優先級下運。如果某個線程處於空閒中,將添加一個任務進來,讓空閒線程去處理任務。如果所有線程都很繁忙,消息隊列會掛起,等待某個線程池空閒後再處理任務。這樣可以保證線程數量不能超多最大數量。
    • 廣義上的線程池:多線程技術主要是解決處理器單元內多個線程執行的問題,它可以顯著減少處理的單元閒置時間,增加處理器單元的吞吐能力。如果對多線程應用不當,會增加對單個任務的的處理時間。
  • 舉一個例子
    • 假如一個服務器完成一項任務的時間爲T:
      • T1 創建線程的時間
      • T2 在線程中執行任務的時間,包括線程同步所需要的時間
      • T3 線程銷燬的時間
    • 顯然 T= T1+T2+T3. 注意:這是一個理想化的情況
      • 可以看出,T1,T3是多線程自身帶來的開銷(在Java中,通過映射pThread,並進一步通過SystemCall實現native線程),我們渴望減少T1和T3的時間,從而減少T的時間。但是一些線程的使用者並沒有注意到這一點,所以在線程中頻繁的創建或者銷燬線程,這導致T1和T3在T中佔有相當比例。這顯然突出的線程池的弱點(T1,T3),而不是有點(併發性)。
    • 所以線程池的技術正是如何關注縮短或調整T1,T3時間的技術,從而提高服務器程序的性能。
      • 1、通過對線程進行緩存,減少創建和銷燬時間的損失
      • 2、通過控制線程數量的閥值,減少線程過少帶來的CPU閒置(比如長時間卡在I/O上了)與線程過多給JVM內存與線程切換時系統調用的壓力。

02.Dispatcher類詳解

2.1 線程池executeService

  • 首先看一下Dispatcher類這個代碼
    /** Executes calls. Created lazily. */
    private ExecutorService executorService;
    
    public synchronized ExecutorService executorService() {
       if (executorService == null) {
         executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
       }
       return executorService;
    }
    
  • 由上面代碼可以得出Dispatcher內部實現了懶加載的無邊界限制的線程池。參數解析
    • 1、0:核心線程數量,保持在線程池中的線程數量(即使已經空閒),爲0代表線程空閒後不會保留,等待一段時間後停止。
    • 2、Integer.MAX_VALUE:表示線程池可以容納最大線程數量
    • 3、TimeUnit.SECOND:當線程池中的線程數量大於核心線程時,空閒的線程就會等待60s纔會被終止,如果小於,則會立刻停止。
    • 4、new SynchronousQueue():線程等待隊列。同步隊列,按序排隊,先來先服務
    • 5、Util.threadFactory(“OkHttp Dispatcher”, false):線程工廠,直接創建一個名爲OkHttp Dispatcher的非守護線程。
  • (1)SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此隊列內部其實沒有任何一個元素,或者說容量爲0,嚴格說並不是一種容器,由於隊列沒有容量,因此不能調用peek等操作,因此只有移除元素纔有元素,顯然這是一種快速傳遞元素的方式,也就是說在這種情況下元素總是以最快的方式從插入者(生產者)傳遞給移除者(消費者),這在多任務隊列中最快的處理任務方式。對於高頻請求場景,無疑是最合適的。
  • (2)在OKHttp中,創建了一個閥值是Integer.MAX_VALUE的線程池,它不保留任何最小線程,隨時創建更多的線程數,而且如果線程空閒後,只能多活60秒。所以也就說如果收到20個併發請求,線程池會創建20個線程,當完成後的60秒後會自動關閉所有20個線程。他這樣設計成不設上限的線程,以保證I/O任務中高阻塞低佔用的過程,不會長時間卡在阻塞上。

2.2 發起請求

  • 整個框架主要通過Call來封裝每一次的請求。同時Call持有OkHttpClient和一份Request。而每一次的同步或者異步請求都會有Dispatcher的參與。
  • (1)、同步
    • Dispatcher在執行同步的Call:直接加入到runningSyncCall隊列中,實際上並沒有執行該Call,而是交給外部執行。
    synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
    }
    
  • (2)、異步
    • 將Call加入隊列:如果當前正在執行的call的數量大於maxRequest(64),或者該call的Host上的call超過maxRequestsPerHos(5),則加入readyAsyncCall排隊等待,否則加入runningAsyncCalls並執行。
    synchronized void enqueue(AsyncCall call) {
        if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
          runningAsyncCalls.add(call);
          executorService().execute(call);
        } else {
          readyAsyncCalls.add(call);
        }
    }
    

2.3 結束請求

  • 從ready到running,在每個call結束的時候都會調用finished。
    • 可以知道finished先執行calls.remove(call)刪除call,然後執行promoteCalls(),在promoteCalls()方法裏面:如果當前線程大於maxRequest則不操作,如果小於maxRequest則遍歷readyAsyncCalls,取出一個call,並把這個call放入runningAsyncCalls,然後執行execute。在遍歷過程中如果runningAsyncCalls超過maxRequest則不再添加,否則一直添加。
    private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
        int runningCallsCount;
        Runnable idleCallback;
        synchronized (this) {
          if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
          if (promoteCalls) promoteCalls();
          runningCallsCount = runningCallsCount();
          idleCallback = this.idleCallback;
        }
    
        if (runningCallsCount == 0 && idleCallback != null) {
          idleCallback.run();
        }
    }
    
    private void promoteCalls() {
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
          AsyncCall call = i.next();
    
          if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            runningAsyncCalls.add(call);
            executorService().execute(call);
          }
    
          if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
        }
    }
    
  • promoteCalls()負責ready的Call到running的Call的轉化。
    • 具體的執行請求則在RealCall裏面實現的,同步的在RealCall的execute裏面實現的,而異步的則在AsyncCall的execute裏面實現的。裏面都是調用RealCall的getResponseWithInterceptorChain的方法來實現責任鏈的調用。

03.OKHttp的任務調度

3.1 Dispatcher任務調度

  • 在OKHttp中,它使用Dispatcher作爲任務的調度器。
    • image
  • 在整個調度流程中涉及的成員如下:
    • Dispatcher 對象是分發者,也是生產者(默認在主線程中)
    • AsyncCall 對象其實是一個任務即Runnable(內部做了包裝異步接口)
  • 類成員屬性大概有這些
    // Dispatcher.java 
    maxRequests = 64   // 最大併發請求數爲64
    maxRequestsPerHost = 5 //每個主機最大請求數爲5
    ExecutorService executorService  //消費者池(也就是線程池)
    Deque<AsyncCall> readyAsyncCalls: // 異步的緩存,正在準備被消費的(用數組實現,可自動擴容,無大小限制)
    Deque<AsyncCall> runningAsyncCalls //正在運行的 異步的任務集合,僅僅是用來引用正在運行的任務以判斷併發量,注意它並不是消費者緩存
    Deque<RealCall> runningSyncCalls  //正在運行的,同步的任務集合。僅僅是用來引用正在運行的同步任務以判斷併發量
    

3.2 OKHttp同步調度流程分析

  • 第一步是:是調用了RealCall的execute()方法裏面調用executed(this);
    @Override public Response execute() throws IOException {
        synchronized (this) {
          if (executed) throw new IllegalStateException("Already Executed");
          executed = true;
        }
        captureCallStackTrace();
        try {
          client.dispatcher().executed(this);
          Response result = getResponseWithInterceptorChain();
          if (result == null) throw new IOException("Canceled");
          return result;
        } finally {
          client.dispatcher().finished(this);
        }
    }
    
  • 第二步:在Dispatcher裏面的executed執行入隊操作
     /** Used by {@code Call#execute} to signal it is in-flight. */
    synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
    }
    
  • 第三步:執行getResponseWithInterceptorChain();進入攔截器鏈流程,然後進行請求,獲取Response,並返回Response result 。
  • 第四步:執行client.dispatcher().finished(this)操作
    void finished(RealCall call) {
        finished(runningSyncCalls, call, false);
    }
    

3.3 OKHttp異步調度流程分析

  • AsyncCall類簡介。在講解異步調度之前不得不提到AsyncCall這個類,AsyncCall,他其實是RealCall的內部類
    //RealCall.java
    final class AsyncCall extends NamedRunnable {
        private final Callback responseCallback;
    
        AsyncCall(Callback responseCallback) {
          super("OkHttp %s", redactedUrl());
          this.responseCallback = responseCallback;
        }
    
        String host() {
          return originalRequest.url().host();
        }
    
        Request request() {
          return originalRequest;
        }
    
        RealCall get() {
          return RealCall.this;
        }
    
        @Override protected void execute() {
          boolean signalledCallback = false;
          try {
            //執行耗時任務
            Response response = getResponseWithInterceptorChain();
            if (retryAndFollowUpInterceptor.isCanceled()) {
              //retryAndFollowUpInterceptor取消了 執行失敗
              signalledCallback = true;
             //回調,注意這裏回調是在線程池中,不是主線程
              responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
            } else {
              //一切正常走入正常流程
              signalledCallback = true;
              responseCallback.onResponse(RealCall.this, response);
            }
          } catch (IOException e) {
            if (signalledCallback) {
              // Do not signal the callback twice!
              Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
            } else {
              responseCallback.onFailure(RealCall.this, e);
            }
          } finally {
           //最後執行出隊
            client.dispatcher().finished(this);
          }
        }
    }
    
  • 第一步 是調用了RealCall的enqueue()方法
    • 在enqueue裏面調用了client.dispatcher().enqueue(new AsyncCall(responseCallback));方法
    @Override public void enqueue(Callback responseCallback) {
        synchronized (this) {
          if (executed) throw new IllegalStateException("Already Executed");
          executed = true;
        }
        captureCallStackTrace();
        client.dispatcher().enqueue(new AsyncCall(responseCallback));
    }
    
  • 第二步:在Dispatcher裏面的enqueue執行入隊操作
    synchronized void enqueue(AsyncCall call) {
        //判斷是否滿足入隊的條件(立即執行)
        if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
          //正在運行的異步集合添加call
          runningAsyncCalls.add(call);
          //執行這個call
          executorService().execute(call);
        } else {
          //不滿足入隊(立即執行)條件,則添加到等待集合中
          readyAsyncCalls.add(call);
        }
    }
    
    • 上述代碼發現想要入隊需要滿足下面的條件:(runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost)
    • 如果滿足條件,那麼就直接把AsyncCall直接加到runningCalls的隊列中,並在線程池中執行(線程池會根據當前負載自動創建,銷燬,緩存相應的線程)。反之就放入readyAsyncCalls進行緩存等待。
    • runningAsyncCalls.size() < maxRequests 表示當前正在運行的AsyncCall是否小於maxRequests = 64
    • runningCallsForHost(call) < maxRequestsPerHos 表示同一個地址訪問的AsyncCall是否小於maxRequestsPerHost = 5;即 當前正在併發的請求不能超過64且同一個地址的訪問不能超過5個
  • 可以看到第二步中分爲兩種情況
    • 可以直接入隊
    • 不能直接入隊,需要等待
3.3.1 第一種情況
  • 第三步:可以直接入隊
    runningAsyncCalls.add(call);
    
  • 第四步:線程池executorService執行execute()方法
    • 由於AsyncCall繼承於NamedRunnable類,而NamedRunnable類又是Runnable類的實現類,所以走到了AsyncCall的execute()方法裏面。
    executorService().execute(call);
    
  • 第五步:執行AsyncCall的execute()方法
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
    
  • 第六步:執行getResponseWithInterceptorChain();進入攔截器鏈流程,然後進行請求,獲取Response。
  • 第七步:如果是正常的獲取到Response,則執行responseCallback.onResponse()
  • 第八步:執行client.dispatcher().finished(this)操作 進行出隊操作
    • 注意這裏面第三個參數 同步是false,異步是true,如果是異步則需要進行是否添加繼續入隊的情景
    void finished(AsyncCall call) {
        finished(runningAsyncCalls, call, true);
    }
    
3.3.2 第二種情況
  • 第三步 不能直接入隊,需要等待
    readyAsyncCalls.add(call);
    
  • 第四步 觸發條件
    • 能進入等待則說明當前要麼有64條正在進行的併發,要麼同一個地址有5個請求,所以要等待。
    • 當有如下條件被滿足或者觸發的時候則執行promoteCalls操作
      • 1 Dispatcher的setMaxRequestsPerHost()方法被調用時
      public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
          //設置的maxRequestsPerHost不能小於1
          if (maxRequestsPerHost < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
          }
          this.maxRequestsPerHost = maxRequestsPerHost;
          promoteCalls();
      }
      
      • 2 Dispatcher的setMaxRequests()被調用時
      public synchronized void setMaxRequests(int maxRequests) {
           //設置的maxRequests不能小於1
          if (maxRequests < 1) {
            throw new IllegalArgumentException("max < 1: " + maxRequests);
          }
          this.maxRequests = maxRequests;
          promoteCalls();
      }
      
      • 3當有一條請求結束了,執行了finish()的出隊操作,這時候會觸發promoteCalls()進行調整
      if (promoteCalls) 
          promoteCalls();
      
  • 第五步 執行Dispatcher的promoteCalls()方法
    private void promoteCalls() {
        if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
        if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
          AsyncCall call = i.next();
    
          if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            runningAsyncCalls.add(call);
            executorService().execute(call);
          }
    
          if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
        }
    }
    
  • 第六步 先判斷是否滿足 初步入隊條件
    • 如果此時 併發的數量還是大於maxRequests=64則return並繼續等待
    • 如果此時,沒有等待的任務,則直接return並繼續等待
    if (runningAsyncCalls.size() >= maxRequests) 
        return;
    if (readyAsyncCalls.isEmpty()) 
        return; // No ready calls to promote.
    
  • 第七步 滿足初步的入隊條件,進行遍歷,然後進行第二輪入隊判斷
    • 進行同一個host是否已經有5請求在了,如果在了,則return返回並繼續等待。
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();
    
      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }
    
      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
    
  • 第八步 此時已經全部滿足條件,則從等待隊列面移除這個call,然後添加到正在運行的隊列中
    i.remove();
    runningAsyncCalls.add(call);
    
  • 第九步 線程池executorService執行execute()方法
    executorService().execute(call);
    
  • 第十步:執行AsyncCall的execute()方法
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
    
  • 第十一步:執行getResponseWithInterceptorChain();進入攔截器鏈流程,然後進行請求,獲取Response。
  • 第十二步:如果是正常的獲取到Response,則執行responseCallback.onResponse()
  • 第十三步:執行client.dispatcher().finished(this)操作 進行出隊操作
    void finished(AsyncCall call) {
        finished(runningAsyncCalls, call, true);
    }
    

3.4 總結一下

  • 1、異步流程總結,所以簡單的描述下異步調度爲:如果當前還能可以執行異步任務,則入隊,並立即執行,否則加入readyAsyncCalls隊列,當一個請求執行完畢後,會調用promoteCalls(),來把readyAsyncCalls隊列中的Async移出來並加入到runningAsyncCalls,並開始執行。然後在當前線程中去執行Call的getResponseWithInterceptorChain()方法,直接獲取當前的返回數據Response
  • 2、對比同步和異步任務,我們會發現:同步請求和異步請求原理都是一樣的,都是在getResponseWithInterceptorChain()函數通過Interceptor鏈條來實現網絡請求邏輯,而異步任務則通過ExecutorService來實現的。PS:在Dispatcher中添加一個封裝了Callback的Call的匿名內部類AsyncCall來執行當前 的Call。這個AsyncCall是Call的匿名內部類。AsyncCall的execute方法仍然會回調到Call的 getResponseWithInterceptorChain方法來完成請求,同時將返回數據或者狀態通過Callback來完成。

04.OKHttp調度優雅之處

  • 1、採用Dispacher作爲調度,與線程池配合實現了高併發,低阻塞的的運行
  • 2、採用Deque作爲集合,按照入隊的順序先進先出
  • 3、最精彩的就是在try/catch/finally中調用finished函數,可以主動控制隊列的移動。避免了使用鎖而wait/notify操作。
  • image
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章