媽媽再也不用擔心你不會使用執行緒池了(ThreadUtils)

johnchou發表於2021-09-09

為什麼要用執行緒池

使用執行緒池管理執行緒有如下優點:

  1. 降低資源消耗:通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  2. 提高響應速度:當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性:執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

執行緒池介紹

ThreadPoolExecutor

Java 為我們提供了 ThreadPoolExecutor 來建立一個執行緒池,其完整建構函式如下所示:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
複製程式碼
  • int corePoolSize(核心執行緒數):執行緒池新建執行緒的時候,如果當前執行緒總數小於corePoolSize,則新建的是核心執行緒,如果超過corePoolSize,則新建的是非核心執行緒;核心執行緒預設情況下會一直存活線上程池中,即使這個核心執行緒啥也不幹(閒置狀態);如果設定了 allowCoreThreadTimeOut 為 true,那麼核心執行緒如果不幹活(閒置狀態)的話,超過一定時間(時長下面引數決定),就會被銷燬掉。

  • int maximumPoolSize(執行緒池能容納的最大執行緒數量):執行緒總數 = 核心執行緒數 + 非核心執行緒數。

  • long keepAliveTime(非核心執行緒空閒存活時長):非核心執行緒空閒時長超過該時長將會被回收,主要應用在快取執行緒池中,當設定了 allowCoreThreadTimeOut 為 true 時,對核心執行緒同樣起作用。

  • TimeUnit unit(keepAliveTime 的單位):它是一個列舉型別,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)。

  • BlockingQueue workQueue(任務佇列):當所有的核心執行緒都在幹活時,新新增的任務會被新增到這個佇列中等待處理,如果佇列滿了,則新建非核心執行緒執行任務,常用的 workQueue 型別:

    1. SynchronousQueue:這個佇列接收到任務的時候,會直接提交給執行緒處理,而不保留它,如果所有執行緒都在工作怎麼辦?那就新建一個執行緒來處理這個任務!所以為了保證不出現 執行緒數達到了 maximumPoolSize 而不能新建執行緒 的錯誤,使用這個型別佇列的時候,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即無限大。

    2. LinkedBlockingQueue:這個佇列接收到任務的時候,如果當前執行緒數小於核心執行緒數,則新建執行緒(核心執行緒)處理任務;如果當前執行緒數等於核心執行緒數,則進入佇列等待。由於這個佇列沒有最大值限制,即所有超過核心執行緒數的任務都將被新增到佇列中,這也就導致了 maximumPoolSize 的設定失效,因為匯流排程數永遠不會超過 corePoolSize。

    3. ArrayBlockingQueue:可以限定佇列的長度,接收到任務的時候,如果沒有達到 corePoolSize 的值,則新建執行緒(核心執行緒)執行任務,如果達到了,則入隊等候,如果佇列已滿,則新建執行緒(非核心執行緒)執行任務,又如果匯流排程數到了 maximumPoolSize,並且佇列也滿了,則發生錯誤。

    4. DelayQueue:佇列內元素必須實現 Delayed 介面,這就意味著你傳進去的任務必須先實現 Delayed 介面。這個佇列接收到任務時,首先先入隊,只有達到了指定的延時時間,才會執行任務。

  • ThreadFactory threadFactory(執行緒工廠):用來建立執行緒池中的執行緒,通常用預設的即可。

  • RejectedExecutionHandler handler(拒絕策略):線上程池已經關閉的情況下和任務太多導致最大執行緒數和任務佇列已經飽和,無法再接收新的任務,在上面兩種情況下,只要滿足其中一種時,在使用 execute() 來提交新的任務時將會拒絕,執行緒池提供了以下 4 種策略:

    1. AbortPolicy:預設策略,在拒絕任務時,會丟擲RejectedExecutionException。

    2. CallerRunsPolicy:只要執行緒池未關閉,該策略直接在呼叫者執行緒中,執行當前的被丟棄的任務。

    3. DiscardOldestPolicy:該策略將丟棄最老的一個請求,也就是即將被執行的任務,並嘗試再次提交當前任務。

    4. DiscardPolicy:該策略默默的丟棄無法處理的任務,不予任何處理。

執行緒池執行策略

當一個任務要被新增進執行緒池時,有以下四種執行策略:

  1. 執行緒數量未達到 corePoolSize,則新建一個執行緒(核心執行緒)執行任務。
  2. 執行緒數量達到了 corePoolsSize,則將任務移入佇列等待。
  3. 佇列已滿,新建非核心執行緒執行任務。
  4. 佇列已滿,匯流排程數又達到了 maximumPoolSize,就會由 RejectedExecutionHandler 丟擲異常。

其流程圖如下所示:

媽媽再也不用擔心你不會使用執行緒池了(ThreadUtils)

常見的四類執行緒池

常見的四類執行緒池分別有 FixedThreadPool、SingleThreadExecutor、ScheduledThreadPool 和 CachedThreadPool,它們其實都是通過 ThreadPoolExecutor 建立的,其引數如下表所示:

引數 FixedThreadPool SingleThreadExecutor ScheduledThreadPool CachedThreadPool
corePoolSize nThreads 1 corePoolSize 0
maximumPoolSize nThreads 1 Integer.MAX_VALUE Integer.MAX_VALUE
keepAliveTime 0 0 10 60
unit MILLISECONDS MILLISECONDS MILLISECONDS SECONDS
workQueue LinkedBlockingQueue LinkedBlockingQueue DelayedWorkQueue SynchronousQueue
threadFactory defaultThreadFactory defaultThreadFactory defaultThreadFactory defaultThreadFactory
handler defaultHandler defaultHandler defaultHandler defaultHandler
適用場景 已知併發壓力的情況下,對執行緒數做限制 需要保證順序執行的場景,並且只有一個執行緒在執行 需要多個後臺執行緒執行週期任務的場景 處理執行時間比較短的任務

如果你不想自己寫一個執行緒池,那麼你可以從上面看看有沒有符合你要求的(一般都夠用了),如果有,那麼很好你直接用就行了,如果沒有,那你就老老實實自己去寫一個吧。

合理地配置執行緒池

需要針對具體情況而具體處理,不同的任務類別應採用不同規模的執行緒池,任務類別可劃分為 CPU 密集型任務、IO 密集型任務和混合型任務。

  • CPU 密集型任務:執行緒池中執行緒個數應儘量少,推薦配置為 (CPU 核心數 + 1);

  • IO 密集型任務:由於 IO 操作速度遠低於 CPU 速度,那麼在執行這類任務時,CPU 絕大多數時間處於空閒狀態,那麼執行緒池可以配置儘量多些的執行緒,以提高 CPU 利用率,推薦配置為 (2 * CPU 核心數 + 1);

  • 混合型任務:可以拆分為 CPU 密集型任務和 IO 密集型任務,當這兩類任務執行時間相差無幾時,通過拆分再執行的吞吐率高於序列執行的吞吐率,但若這兩類任務執行時間有資料級的差距,那麼沒有拆分的意義。

執行緒池工具類封裝及使用

為了提升開發效率及更好地使用和管理執行緒池,我已經為你們封裝好了執行緒工具類----ThreadUtils,依賴 AndroidUtilCode 1.16.1 版本即可使用,其 API 如下所示:

isMainThread            : 判斷當前是否主執行緒
getFixedPool            : 獲取固定執行緒池
getSinglePool           : 獲取單執行緒池
getCachedPool           : 獲取緩衝執行緒池
getIoPool               : 獲取 IO 執行緒池
getCpuPool              : 獲取 CPU 執行緒池
executeByFixed          : 在固定執行緒池執行任務
executeByFixedWithDelay : 在固定執行緒池延時執行任務
executeByFixedAtFixRate : 在固定執行緒池按固定頻率執行任務
executeBySingle         : 在單執行緒池執行任務
executeBySingleWithDelay: 在單執行緒池延時執行任務
executeBySingleAtFixRate: 在單執行緒池按固定頻率執行任務
executeByCached         : 在緩衝執行緒池執行任務
executeByCachedWithDelay: 在緩衝執行緒池延時執行任務
executeByCachedAtFixRate: 在緩衝執行緒池按固定頻率執行任務
executeByIo             : 在 IO 執行緒池執行任務
executeByIoWithDelay    : 在 IO 執行緒池延時執行任務
executeByIoAtFixRate    : 在 IO 執行緒池按固定頻率執行任務
executeByCpu            : 在 CPU 執行緒池執行任務
executeByCpuWithDelay   : 在 CPU 執行緒池延時執行任務
executeByCpuAtFixRate   : 在 CPU 執行緒池按固定頻率執行任務
executeByCustom         : 在自定義執行緒池執行任務
executeByCustomWithDelay: 在自定義執行緒池延時執行任務
executeByCustomAtFixRate: 在自定義執行緒池按固定頻率執行任務
cancel                  : 取消任務的執行
複製程式碼

如果你使用 RxJava 很 6,而且專案中已經使用了 RxJava,那麼你可以繼續使用 RxJava 來做執行緒切換的操作;如果你並不會 RxJava 或者是在開發 SDK,那麼這個工具類再適合你不過了,它可以為你統一管理執行緒池的使用,不至於讓你的專案中出現過多的執行緒池。

ThreadUtils 使用極為方便,看 API 即可明白相關意思,FixedPool、SinglePool、CachedPool 分別對應了上面介紹的 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 這三種,IoPool 是建立 (CPU_COUNT * 2 + 1) 個核心執行緒數,CpuPool 是建立 (CPU_COUNT + 1) 個核心執行緒數;而所有的 execute 都是執行緒池外圍裹了一層 ScheduledThreadPool,這裡和 RxJava 執行緒池的實現有所相似,可以更方便地提供延時任務和固定頻率執行的任務,當然也可以更方便地取消任務的執行,下面讓我們來簡單地來介紹其使用,以從 assets 中拷貝 APK 到 SD 卡為例,其程式碼如下所示:

public static void releaseInstallApk(final OnReleasedListener listener) {
    if (!FileUtils.isFileExists(Config.TEST_APK_PATH)) {
        ThreadUtils.executeByIo(new ThreadUtils.SimpleTask<Void>() {
            @Override
            public Void doInBackground() throws Throwable {
                ResourceUtils.copyFileFromAssets("test_install", Config.TEST_APK_PATH);
                return null;
            }

            @Override
            public void onSuccess(Void result) {
                if (listener != null) {
                    listener.onReleased();
                }
            }
        });
    } else {
        if (listener != null) {
            listener.onReleased();
        }
        LogUtils.d("test apk existed.");
    }
}
複製程式碼

看起來還不是很優雅是吧,你可以把相關的 Task 都抽出來放到合適的包下,這樣每個 Task 的職責一看便知,如上例子可以改裝成如下所示:

public class ReleaseInstallApkTask extends ThreadUtils.SimpleTask<Void> {

    private OnReleasedListener mListener;

    public ReleaseInstallApkTask(final OnReleasedListener listener) {
        mListener = listener;
    }

    @Override
    public Void doInBackground() throws Throwable {
        ResourceUtils.copyFileFromAssets("test_install", Config.TEST_APK_PATH);
        return null;
    }

    @Override
    public void onSuccess(Void result) {
        if (mListener != null) {
            mListener.onReleased();
        }
    }

    public void execute() {
        ThreadUtils.executeByIo(this);
    }
}

public static void releaseInstallApk(final OnReleasedListener listener) {
    if (!FileUtils.isFileExists(Config.TEST_APK_PATH)) {
        new ReleaseInstallApkTask(listener).execute();
    } else {
        if (listener != null) {
            listener.onReleased();
        }
        LogUtils.d("test apk existed.");
    }
}
複製程式碼

是不是瞬間清爽了很多,如果執行成功的回撥中涉及了 View 相關的操作,那麼你需要在 destroy 中取消 task 的執行哦,否則會記憶體洩漏哦,繼續以上面的例子為例,程式碼如下所示:

public class XXActivity extends Activity {
    ···
    
    @Override
    protected void onDestroy() {
        // ThreadUtils.cancel(releaseInstallApkTask);// 或者下面的取消都可以
        releaseInstallApkTask.cancel();
        super.onDestroy();
    }
}
複製程式碼

以上是以 SimpleTask 為例,Task 的話會多兩個回撥,onCancel() 和 onFail(Throwable t),它們和 onSuccess(T result) 都是互斥的,最終回撥只會走它們其中之一,並且在 Android 端是傳送到主執行緒中執行,如果是 Java 端的話那就還是會在相應的執行緒池中執行,這點也方便了我做單元測試。

執行緒池工具類單元測試

如果遇到了非同步的單測,你會發現單測很快就跑完呢,並沒有等待我們執行緒跑完再結束,我們可以用 CountDownLatch 來等待執行緒的結束,或者化非同步為同步的做法,這裡我們使用 CountDownLatch 來實現,我進行了簡單的封裝,測試 Fixed 的程式碼如下所示:

public class ThreadUtilsTest {

    @Test
    public void executeByFixed() throws Exception {
        asyncTest(10, new TestRunnable<String>() {
            @Override
            public void run(final int index, CountDownLatch latch) {
                final TestTask<String> task = new TestTask<String>(latch) {
                    @Override
                    public String doInBackground() throws Throwable {
                        Thread.sleep(500 + index * 10);
                        if (index < 4) {
                            return Thread.currentThread() + " :" + index;
                        } else if (index < 7) {
                            cancel();
                            return null;
                        } else {
                            throw new NullPointerException(String.valueOf(index));
                        }
                    }

                    @Override
                    void onTestSuccess(String result) {
                        System.out.println(result);
                    }
                };
                ThreadUtils.executeByFixed(3, task);
            }
        });
    }

    @Test
    public void executeByFixedWithDelay() throws Exception {
        asyncTest(10, new TestRunnable<String>() {
            @Override
            public void run(final int index, CountDownLatch latch) {
                final TestTask<String> task = new TestTask<String>(latch) {
                    @Override
                    public String doInBackground() throws Throwable {
                        Thread.sleep(500);
                        if (index < 4) {
                            return Thread.currentThread() + " :" + index;
                        } else if (index < 7) {
                            cancel();
                            return null;
                        } else {
                            throw new NullPointerException(String.valueOf(index));
                        }
                    }

                    @Override
                    void onTestSuccess(String result) {
                        System.out.println(result);
                    }
                };
                ThreadUtils.executeByFixedWithDelay(3, task, 500 + index * 10, TimeUnit.MILLISECONDS);
            }
        });
    }

    @Test
    public void executeByFixedAtFixRate() throws Exception {
        asyncTest(10, new TestRunnable<String>() {
            @Override
            public void run(final int index, CountDownLatch latch) {
                final TestScheduledTask<String> task = new TestScheduledTask<String>(latch, 3) {
                    @Override
                    public String doInBackground() throws Throwable {
                        Thread.sleep(500 + index * 10);
                        if (index < 4) {
                            return Thread.currentThread() + " :" + index;
                        } else if (index < 7) {
                            cancel();
                            return null;
                        } else {
                            throw new NullPointerException(String.valueOf(index));
                        }
                    }

                    @Override
                    void onTestSuccess(String result) {
                        System.out.println(result);
                    }
                };
                ThreadUtils.executeByFixedAtFixRate(3, task, 3000 + index * 10, TimeUnit.MILLISECONDS);
            }
        });
    }

    abstract static class TestScheduledTask<T> extends ThreadUtils.Task<T> {

        private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();
        private int mTimes;
        CountDownLatch mLatch;

        TestScheduledTask(final CountDownLatch latch, final int times) {
            mLatch = latch;
            mTimes = times;
        }

        abstract void onTestSuccess(T result);

        @Override
        public void onSuccess(T result) {
            onTestSuccess(result);
            if (ATOMIC_INTEGER.addAndGet(1) % mTimes == 0) {
                mLatch.countDown();
            }
        }

        @Override
        public void onCancel() {
            System.out.println(Thread.currentThread() + " onCancel: ");
            mLatch.countDown();
        }

        @Override
        public void onFail(Throwable t) {
            System.out.println(Thread.currentThread() + " onFail: " + t);
            mLatch.countDown();
        }
    }

    abstract static class TestTask<T> extends ThreadUtils.Task<T> {
        CountDownLatch mLatch;

        TestTask(final CountDownLatch latch) {
            mLatch = latch;
        }

        abstract void onTestSuccess(T result);

        @Override
        public void onSuccess(T result) {
            onTestSuccess(result);
            mLatch.countDown();
        }

        @Override
        public void onCancel() {
            System.out.println(Thread.currentThread() + " onCancel: ");
            mLatch.countDown();
        }

        @Override
        public void onFail(Throwable t) {
            System.out.println(Thread.currentThread() + " onFail: " + t);
            mLatch.countDown();
        }
    }

    <T> void asyncTest(int threadCount, TestRunnable<T> runnable) throws Exception {
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            runnable.run(i, latch);
        }
        latch.await();
    }

    interface TestRunnable<T> {
        void run(final int index, CountDownLatch latch);
    }
}
複製程式碼

最後想說的話

感謝大家一起陪伴 AndroidUtilCode 的成長,核心工具類幾乎都已囊括,也是彙集了我大量的心血,把開源做到了極致,希望大家可以用的舒心,大大提升開發效率,早日贏取白富美,走上人生巔峰。

歡迎來我的 狗窩 坐坐哈

後文再新增一個個人對 OkHttp 的執行緒池的使用分析,算是送上個小福利。

OkHttp 中的執行緒池使用

檢視 OkHttp 的原始碼發現,不論是同步請求還是非同步請求,最終都是交給 Dispatcher 做處理,我們看下該類和執行緒池有關的的主要程式碼:

public final class Dispatcher {
  // 最大請求數
  private int maxRequests = 64;
  // 相同 host 最大請求數
  private int maxRequestsPerHost = 5;
  // 請求執行執行緒池,懶載入
  private @Nullable ExecutorService executorService;
  // 就緒狀態的非同步請求佇列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  // 執行中的非同步請求佇列,包括還沒完成的請求
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
      this.executorService = executorService;
  }

  public Dispatcher() {
  }

  public synchronized ExecutorService executorService() {
      if (executorService == null) {
          // 和 CachedThreadPool 很相似
          executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
      }
      return executorService;
  }

  synchronized void enqueue(AsyncCall call) {
    // 不超過最大請求數並且不超過 host 最大請求數
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      // 新增到執行中的非同步請求佇列
      runningAsyncCalls.add(call);
      // 新增到執行緒池中執行
      executorService().execute(call);
    } else {
      // 新增到就緒的非同步請求佇列
      readyAsyncCalls.add(call);
    }
  }

  // 當該非同步請求結束的時候,會呼叫此方法,用於將執行中的非同步請求佇列中的該請求移除並調整請求佇列
  // 此時就緒佇列中的請求就可以進入執行中的佇列
  void finished(AsyncCall call) {
      finished(runningAsyncCalls, call, true);
  }

  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();
      }
  }

  // 根據 maxRequests 和 maxRequestsPerHost 來調整 runningAsyncCalls 和 readyAsyncCalls
  // 使執行中的非同步請求不超過兩種最大值,並且如果佇列有空閒,將就緒狀態的請求歸類為執行中。
  private void promoteCalls() {
    // 如果執行中的非同步佇列不小於最大請求數,直接返回
    if (runningAsyncCalls.size() >= maxRequests) return;
    // 如果就緒佇列為空,直接返回
    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;
    }
  }
}
複製程式碼

可以發現 OkHttp 不是線上程池中維護執行緒的個數,執行緒是通過 Dispatcher 間接控制,執行緒池中的請求都是執行中的請求,這也就是說執行緒的重用不是執行緒池控制的,通過原始碼我們發現執行緒重用的地方是請求結束的地方 finished(AsyncCall call) ,而真正的控制是通過 promoteCalls 方法, 根據 maxRequestsmaxRequestsPerHost 來調整 runningAsyncCallsreadyAsyncCalls,使執行中的非同步請求不超過兩種最大值,並且如果佇列有空閒,將就緒狀態的請求歸類為執行中。

相關文章