1. 程式人生 > >Android 執行緒池的使用

Android 執行緒池的使用

執行緒池優點
提到執行緒池就必須先說一下執行緒池的優點,執行緒池的優點可以概括為以下四點:
* 重用執行緒池中的執行緒,避免因為執行緒的建立和銷燬所帶來的效能開銷;
* 執行緒池旨線上程的複用,就避免了建立執行緒和銷燬執行緒所帶來的時間消耗,減少執行緒頻繁排程的開銷,從而節約系統資源,提高系統吞吐量;
* 能有效控制執行緒池的最大併發數,避免大量的執行緒之間因互相搶佔系統資源而導致的阻塞現象;
* 能夠對執行緒進行簡單的管理,並提供定時執行以及指定時間間隔迴圈執行等功能。

執行緒池的建立
Android 中的執行緒池的概念來源於 Java 中的 Executor ,Executor 是一個介面,真正的執行緒池的實現為 ThreadPoolExecutor,ThreadPoolExecutor 提供了一些列的引數來配置執行緒池,通過不同的引數可以建立功能特性不同的執行緒池。我們要建立一個執行緒池只需要 new ThreadPoolExecutor(…) 就可以建立一個執行緒池,但我們如此建立執行緒池的話,需要配置一些引數,非常麻煩,同時 Google 官方也不推薦使用這種方式來建立執行緒池,而是推薦使用 Executors 的工廠方法來建立執行緒池。但 Executors 的工廠方法建立的執行緒池也是直接或間接通過配置 ThreadPoolExecutor 引數來實現的,因此有必要先了解 ThreadPoolExecutor 的配置引數。

ThreadPoolExecutor
ThreadPoolExecutor 的構造方法提供了一些列的引數來配置執行緒池,先來了解一下 ThreadPoolExecutor 的構造方法中各個引數的含義,這些引數將直接影響到執行緒池的功能特性。

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize, 
                              long keepAliveTime, 
                              TimeUnit unit, 
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory, 
                              RejectedExecutionHandler handler) {...
}

corePoolSize 執行緒池的核心執行緒數,預設情況下,核心執行緒會線上程池中一直存活,即使它們處於閒置狀態。但如果將 ThreadPoolExecutor 的 allowCoreThreadTimeOut 屬性設定為 true ,那麼閒置的核心執行緒在等待新任務到來時會有超時策略,這個時間間隔是由 keepAliveTime 所指定,當等待時間超過 keepAliveTime 所指定的時長後,核心執行緒就會被終止。
maximumPoolSize 執行緒池所能容納的最大執行緒數,當活動執行緒數達到這個數值後,後續的新任務將會被阻塞。
keepAliveTime 非核心執行緒閒置時的超時時長,超過這個時長,非核心執行緒就會被回收。當 ThreadPoolExecutor 的 allowCoreThreadTimeOut 屬性設定為 true 時,keepAliveTime 同樣會作用於核心執行緒。
unit

用於指定 keepAliveTime 引數的時間單位,這是一個列舉,常用的有 TimeUnit .MILLISECONDS 和 TimeUnit .SECONDS。
workQueue 執行緒池中的任務佇列,通過執行緒池的 execute 方法提交的 Runnable 物件會儲存在這個引數中。
threadFactory 執行緒工廠,為執行緒池提供建立新的執行緒的功能。threadFactory 是一個介面,它只有一個方法: public abstract Thread newThread (Runnable r);
RejectedExecutionHandler 通常叫做拒絕策略,線上程池已經關閉的情況下 ,或者任務太多導致最大執行緒數和任務佇列已經飽和,無法再接收新的任務 。在上面兩種情況下,只要滿足其中一種時,在使用 execute() 來提交新的任務時將會拒絕,而預設的拒絕策略是拋一個 RejectedExecutionException 異常。

ThreadPoolExecutor 執行任務時策略
以下用 currentSize 表示執行緒池中當前執行緒數量
1. 當 currentSize < corePoolSize 時,將會直接啟動一個核心執行緒來執行任務。
2. 當 currentSize >= corePoolSize ,那麼任務會被插入到任務佇列中排隊等待執行。
3. 如果任務佇列已滿,但 currentSize < maximumPoolSize,那麼會立刻啟動一個非核心執行緒來執行任務
4. 如果任務佇列已滿,並且 currentSize > maximumPoolSize,那麼就拒絕執行任務,ThreadPoolExecutor 會呼叫 RejectedExecutionHandler 的 rejectedExecution 方法來通知呼叫者。
下面通過 demo 來驗證一下:

 ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 30, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(128));
        for (int i = 0; i < 30; i++) {
            final int index = i;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        String threadName = Thread.currentThread().getName();
                        Log.v(TAG, "執行緒:"+threadName+",正在執行第" + index + "個任務");
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
        }

在示例中,核心執行緒數為 3,workQueue 的大小為 128,所以我們的執行緒的執行應該是先啟動三個核心執行緒來執行任務,剩餘的 27 個任務全部存在workQueue 中,等待核心執行緒空餘出來之後執行。
這裡寫圖片描述

那我把構造 ThreadPoolExecutor 的引數修改一下,來驗證一下我們上面的結論正確與否。

 ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 30, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(6));
        for (int i = 0; i < 30; i++) {
            final int index = i;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        String threadName = Thread.currentThread().getName();
                        Log.v(TAG, "執行緒:"+threadName+",正在執行第" + index + "個任務");
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
        }

這裡寫圖片描述
首先打印出來 0,1,2 說明往核心執行緒添加了三個任務,然後將 3,4,5,6,7,8 六個任務新增到了任務佇列中,接下來要新增的任務因為核心執行緒已滿,佇列已滿所以就直接開一個非核心執行緒來執行,因此後新增的任務反而會先執行(3,4,5,6,7,8都在佇列中等著),所以我們看到的列印結果是先是 0~2,然後 9~29,然後 3~8,當然,我們在實際開發中不會這樣來配置最大執行緒數和執行緒佇列。

執行緒池的分類
由於 Google 官方不推薦使用 new 的方式來建立執行緒池,而是推薦使用 Executors 的工廠方法來建立執行緒池,通過直接或間接的配置 ThreadPoolExecutor 的引數來構建執行緒池,常用的執行緒池有如下 4 種,newFixedThreadPool ,newCachedThreadPool,
newScheduledThreadPool 和 newSingleThreadExecutor。

FixedThreadPool 通過 Executors .newFixedThreadPool 來建立。它是一個執行緒數量固定的執行緒池,當執行緒處於空閒狀態時,它們並不會被回收,除非執行緒池被關閉了。當所有的執行緒都處於活動狀態時,新任務都會處於等待狀態,直到有執行緒空閒出來。由於 FixedThreadPool 只有核心執行緒並且這些核心執行緒不會被回收,這意味著它能快速的響應外界的請求。 newFixedThreadPool 方法的實現如下,可以發現 FixedThreadPool 中只有核心執行緒並且這些核心執行緒沒有超時機制,另外任務佇列也是沒有大小限制。

 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

CachedThreadPool 通過 Executors.newCachedThreadPool 來建立,它是一個執行緒數量不定的執行緒池,它只有非核心執行緒,並且最大執行緒數為 Integer.MAX_VALUE ,由於 Integer.MAX_VALUE 是一個很大的數,實際上就相當於最大核心執行緒數可以任意大。當執行緒池中的執行緒都處於活動狀態時,當有新任務過來時,執行緒池就會建立新的執行緒來處理新任務,否則就會利用空閒的執行緒來處理。執行緒池中的空閒執行緒都有超時機制,這個超時時長為 60 秒,超過 60 秒閒置執行緒就會被回收。SynchronousQueue 佇列相當於一個空佇列,這就導致任何任務都會立即執行。

  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

ScheduledThreadPool 通過 Executors.newScheduledThreadPool 來建立,它的核心數量是固定的,非核心執行緒沒有限制,並且當非核心執行緒閒置時會被立即回收,ScheduledThreadPool 這類執行緒池主要用於執行定時任務和具有固定週期的重複任務。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

SingleThreadExecutor 通過 Executors.newSingleThreadExecutor 來建立。這類執行緒池內部只有一個核心執行緒,它確保所有的任務都在同一個執行緒中按順序執行。SingleThreadExecutor 的意義在於統一所有的外界任務到一個執行緒中,這使得這些任務之間不需要處理執行緒同步的問題。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

執行緒池 ThreadPoolExecutor 的使用
使用執行緒池,其中涉及到一個極其重要的方法,即:

execute(Runnable command)
Executes the given task sometime in the future.

該方法意為執行給定的任務,該任務處理可能在新的執行緒、已入池的執行緒或者正呼叫的執行緒,這由 ThreadPoolExecutor 的實現決定。

newFixedThreadPool 建立一個固定執行緒數量的執行緒池,示例為:

 @Override
    public void onClick(View v) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                   Log.v(TAG, "執行緒:"+threadName+",正在執行第" + index + "個任務");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }
    }

在上述程式碼中,建立了執行緒數量固定為 3 的執行緒池,該執行緒池支援的最大執行緒也為 3 ,而我們建立了 10 個任務讓它處理,通過 log 分析,執行的情況是先執行前 3 個任務,後面 7 個任務都進入任務佇列進行等待,執行完前 3 個任務後,再通過 FIFO 的方式從任務佇列中取任務執行,直到所有任務都執行完畢。
這裡寫圖片描述

newSingleThreadExecutor 建立一個只有一個執行緒數的執行緒池。通過 log 分析,每次執行緒池只執行一個任務,其餘的任務都將進入任務佇列進行等待。

@Override
    public void onClick(View v) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    Log.v(TAG, "執行緒:"+threadName+",正在執行第" + index + "個任務");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }
    }

程式碼改動不大,只是改動了 ThreadPoolExecutor 的實現方式,任務都是一個一個的執行,並且都是同一個執行緒。
這裡寫圖片描述

newCachedThreadPool 建立一個可以根據實際情況調整執行緒池中執行緒數量的執行緒池

 @Override
    public void onClick(View v) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    Log.v(TAG, "執行緒:"+threadName+",正在執行第" + index + "個任務");
                    try {
                        long time = index * 500;
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }
    }

為了體現該執行緒池可以自動根據實際情況進行執行緒的重用,而不是一味的建立新的執行緒去處理任務,我設定了每隔 1s 去提交一個新任務,這個新任務執行的時間也是動態變化的。
這裡寫圖片描述
通過 log 可以看出,新增的任務都會被立即執行。

newScheduledThreadPool 建立一個可以定時或週期性執行任務的執行緒池。

@Override
    public void onClick(View v) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);
        threadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                Log.v(TAG, "執行緒:" + threadName + ",正在執行");
            }
        }, 2, 1, TimeUnit.SECONDS);
    }

延遲 2 秒後,每隔 1 秒執行一次該任務
這裡寫圖片描述
從 log 日誌可以看出 ScheduledThreadPool 是 4 個執行緒池裡面唯一一個個有延遲執行和週期重複執行的執行緒池。

總結
1. FixedThreadPool 只有核心執行緒,並且數量是固定的,也不會被回收,能更快地響應外界請求。
2. SingleThreadPool 只有一個核心執行緒,確保所有任務都在同一執行緒中按順序完成。因此不需要處理執行緒同步的問題。
3. CachedThreadPool 只有非核心執行緒,最大執行緒數非常大,所有執行緒都活動時,會為新任務建立新執行緒,否則利用空閒執行緒處理任務,任何任務都會被立即執行。
4. ScheduledThreadPool 核心執行緒數固定,非核心執行緒數沒有限制,主要用於執行定時任務以及有固定週期的重複任務。

好了,關於執行緒池方面的內容今天就分析到這裡,在這篇文章中我們主要學習了執行緒池的基本概念和基本用法。下一篇文章當中,將會繼續帶著大家學習執行緒池的高階用法,講一講自定義執行緒池的知識,敬請期待。
Android 執行緒池高階使用