Android多線程編程之線程池學習篇(一)

來源:互聯網
上載者:User

標籤:

Android多線程編程之線程池學習篇(一)一、前言

Android應用開發中多線程編程應用比較廣泛,而應用比較多的是ThreadPoolExecutor,AsyncTask,IntentService,HandlerThread,AsyncTaskLoader等,為了更詳細的分析每一種實現方式,將單獨成篇分析。後續篇章中可能涉及到線程池的知識,特此本篇分析為何使用線程池,如何使用線程池以及線程池的使用原理。

二、Thread Pool基礎

進程代表一個運行中的程式,一個運行中的Android應用程式就是一個進程。從作業系統的方面來說,線程是進程中可以獨立執行的子任務。一個進程可以有多個線程,同一個進程中的線程可以共用進程中的資源。從JVM的方面來說,線程是進程中的一個組件,是執行java代碼的最小單位。

在Android應用開發過程中,如果需要處理非同步或並發任務時可以使用線程池,使用線程池可以有以下好處:
1、降低資源消耗:線程的建立和銷毀都需要消耗資源,重複利用線程可以避免過度的消耗資源。
2、提高響應速度:當有任務需要執行時,可以不用重新建立線程就能開始執行任務。
3、提高線程的管理性:過多的建立線程會降低系統的穩定性,使用線程池可以統一分配,調優和監控。

Thread Pool模式的原理是使用隊列對待處理的任務進行緩衝,並複用一定數量的工作者線程從隊列中取出任務來執行。其本質是使用有限的資源來處理無限的任務。

Thread Pool模式最核心的類是ThreadPoolExecutor,它是線程池的實作類別。使用Executors可以建立三種類型的ThreadPoolExecutor類,主要是以下三種類型:
(1)FixedThreadPool
(2)SingleThreadExecutor
(3)CachedThreadPool

三、Executor架構分析

Executor介面提供一種將任務提交與每個任務將如何啟動並執行機制(包括線程使用的細節、調度等)分離開來的方法。

Executor架構中主要包括:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future介面、Runable介面、Callable介面和Executors。

是Executor架構的類圖。

==首先主線程建立任務:==任務的對象可以通過實現Runnable介面或者Callable介面實現。而Runnable可以通過Executors.callable(Runnable runnable)或者Executors.callable(Runnable runnable, Object result)方法來轉換封裝為Callable對象。

==其後是執行任務:==執行任務的方式有兩種,一種是execut()方法,執行提交的Runnable對象,ExecutorService.execute(Runnable runnable);另外一種是submit()方法,可以執行提交的Runnable對象,ExecutorService.submit(Runnable runnable),或者是執行提交的Callable對象,ExecutorService.submit(Callable callable)。

==取消任務:==可以選擇使用FetureTask.cancel(boolean flag)取消任務。

==關閉 ExecutorService:==這將導致其拒絕新任務。有兩種方式來關閉 ExecutorService。shutdown() 方法在終止前允許執行以前提交的任務,而 shutdownNow() 方法阻止等待任務啟動並試圖停止當前正在執行的任務。在終止時,執行程式沒有任務在執行,也沒有任務在等待執行,並且無法提交新任務。

注意:應該關閉未使用的 ExecutorService 以允許回收其資源。

下列方法分兩個階段關閉 ExecutorService。第一階段調用 shutdown 拒絕傳入任務,然後調用 shutdownNow(如有必要)取消所有遺留的任務:

void shutdownAndAwaitTermination(ExecutorService pool) {   pool.shutdown(); // Disable new tasks from being submitted   try {     // Wait a while for existing tasks to terminate     if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {       pool.shutdownNow(); // Cancel currently executing tasks       // Wait a while for tasks to respond to being cancelled       if (!pool.awaitTermination(60, TimeUnit.SECONDS))           System.err.println("Pool did not terminate");     }   } catch (InterruptedException ie) {     // (Re-)Cancel if current thread also interrupted     pool.shutdownNow();     // Preserve interrupt status     Thread.currentThread().interrupt();   } }
四、ThreadPoolExecutor原理分析

當向一個線程池中添加一個任務時,線程池是如何工作的?下面根據原始碼來分析其中的原理:

public void execute(Runnable command) {    if (command == null)        throw new NullPointerException();    int c = ctl.get();    if (workerCountOf(c) < corePoolSize) {        if (addWorker(command, true))            return;        c = ctl.get();    }    //如果線程數大於等於基本線程數或者線程建立失敗,則將當前任務放到工作隊列當中。    if (isRunning(c) && workQueue.offer(command)) {        int recheck = ctl.get();        if (! isRunning(recheck) && remove(command))            reject(command);        else if (workerCountOf(recheck) == 0)            addWorker(null, false);    }    //如果線程池不處於運行中或者任務無法放入隊列中,並且當前線程數量小於最大允許的線程數量。則會建立一個線程來執行該任務。    else if (!addWorker(command, false))    //拋出RejectExecutionException異常。        reject(command);}

以下是線程池的主要處理流程圖:


可以看出,當提交一個任務時,
* 首先判斷核心線程池是否都在執行任務,如果還有未執行任務的線程,則會新建立一個核心線程來執行此任務,否則,將進入下一個流程當中。
* 如果儲存任務的隊列沒有滿,那麼任務則會儲存到這個隊列當中,如果該隊列已經滿了,則會進入下一個流程當中。
* 判斷線程池當中是否還有非核心線程沒有處於工作狀態,如果沒有,則會建立一個新線程來執行任務,如果已經滿了,則會進入下一個階段來處理。
* 當線程和隊列都已經滿了的時候,由RejectdeExecutionHandler來處理,處理的方式有四種,如所示:

任務的處理走向從上面這個圖就很明了了。

大概瞭解了ThreadPoolExecutor之後,再來說說如何建立一個線程池。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);

1)corePoolSize(線程池的基本大小,也可以說是核心線程數的大小):如果提交一個任務時,線程池中的核心線程數小於這個基本大小值,或者是存在核心線程處於空閑狀態,則會建立一個線程來執行提交的任務。當線程數量等於基本大小時就不會建立了。
==注意:==當調用了線程池中的prestartAllCoreThreads()方法,線程池會提前建立並啟動所有的基本線程,即所謂的核心線程。

2)maximumPoolSize(線程池的最大數量):即所謂的線程池所能建立的最大線程數量,這裡的數量中包含核心線程和非核心線程。如果隊列中的任務存滿,另外線程數小於線程池的最大數量,那麼會新建立線程類執行任務。
==注意:==當隊列是無界隊列時,則設定線程池的最大數量值就無效了。

3)keepAliveTime(線程活動保持的時間):線程池的背景工作執行緒空閑後,保持存活的時間。
使用情境:當提交的任務過多時,可以設定較大的時間值,充分提高線程的利用率。

4)unit(線程活動保持的時間單位):可以選擇相應的時間單位。如天、時、分、秒、毫秒、微秒、千分之一毫秒和納秒。

5)workQueue(儲存任務的隊列):用於儲存等待的任務的阻塞隊列。這種形式的阻塞隊列常見一下幾種:
ArrayBlockingQueue:基於數組結構的有界阻塞隊列,按先進先出原則排序。
LinkedBlockingQueue:基於鏈表結構的阻塞隊列,按先進先出原則排序。
SynchronousQueue:不儲存元素的阻塞隊列。
PriorityBlockingQueue:具有優先順序的無限阻塞隊列。

6)threadFactory(用於建立線程的工廠):通過原廠模式來建立線程。

7)defaultHandler(飽和策略的處理模式):當隊列和線程池都滿的時候,這種狀態就是處於飽和狀態了,那麼必須採取一種策略來處理不能執行的新任務。關於飽和策略的處理有四種方式:

  • AbortPolicy:直接拋出異常。
  • CallerRunsPolicy:只用調用者所線上程來運行任務。
  • DiscardOldestPolicy:丟棄隊列裡最近的一個任務,並執行當前任務。
  • DiscardPolicy:直接丟棄任務。
五、FixedThreadPool原理分析

FixedThreadPool通過Executors中的newFixedThreadPool()方法來建立的。
這是建立一個可重用固定線程數量的線程池,以共用的無界隊列方式來運行這些線程。在這個線程池中只有核心線程,不存在非核心線程,當核心線程處於空閑狀態時,線程不會被回收,只有當線程池關閉時才會回收。

(1)如果啟動並執行線程數少於corePoolSize,則會建立新的線程倆執行任務,當有任務來不及給線程來處理時,則會將任務添加到任務隊列中。當任務數少於線程數的時候,線程池執行結果如下:

public static void main(String[] args) {        ExecutorService eService = Executors.newFixedThreadPool(40);        for (int i = 0; i < 2; i++) {            final int j = i;            eService.execute(new Runnable() {                @Override                public void run() {                    System.out.println(Thread.currentThread().getName() + " " + j);                }            });        }    }

運行結果:
pool-1-thread-2 1
pool-1-thread-1 0
說明線程池中先只建立兩個線程。

(2)如果任務數量比較多的時候,超過核心線程數,那麼當線程執行完任務後會從隊列中取任務執行。執行個體代碼如下:

public static void main(String[] args) {        ExecutorService eService = Executors.newFixedThreadPool(4);        for (int i = 0; i < 2000; i++) {            final int j = i;            eService.execute(new Runnable() {                @Override                public void run() {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName() + " " + j);                }            });        }    }

運行結果:
pool-1-thread-1 0
pool-1-thread-4 3
pool-1-thread-3 2
pool-1-thread-2 1
pool-1-thread-4 4
pool-1-thread-3 5
pool-1-thread-2 7
pool-1-thread-1 6
pool-1-thread-4 8
pool-1-thread-3 9
pool-1-thread-2 10
pool-1-thread-1 11
pool-1-thread-4 12

從運行結果可以知道,線程池中的線程不會無限建立,數量最多為corePoolSize大小。

六、SingleThreadExecutor原理分析

SingleThreadExecutor是通過使用Executors的newSingleThreadExecutor方法來建立的,以無界隊列方式來運行該線程。這個線程池中內部只有一個核心線程,而且沒有非核心線程。SingleThreadExecutor的原始碼實現如下:

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

其中corePoolSize和maximumPoolSize被設定為1,其它的與FixPoolThread相同。

(1)當線程池中的線程數少於corePoolSize,則會建立一個新線程來執行任務。

(2)如果任務數量比較多的時候,超過核心線程數,那麼當線程執行完任務後會從隊列中取任務執行,並且按照順序執行。執行個體代碼如下:

public static void main(String[] args) {        ExecutorService eService = Executors.newSingleThreadExecutor();        for (int i = 0; i < 20; i++) {            final int j = i;            eService.execute(new Runnable() {                @Override                public void run() {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName() + " " + j);                }            });        }    }

運行結果:
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-1 6
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9
pool-1-thread-1 10
pool-1-thread-1 11
pool-1-thread-1 12
pool-1-thread-1 13
pool-1-thread-1 14
pool-1-thread-1 15
pool-1-thread-1 16
pool-1-thread-1 17
pool-1-thread-1 18
pool-1-thread-1 19
pool-1-thread-1 20

從運行結果可以看出,線程池中只有一個核心線程,另外按照一定的順序執行任務。

七、CachedThreadPool原理分析

CachedThreadPool是使用Executors中的newCachedThreadPool()方法建立,它是一種線程數量不固定的線程池,沒有核心線程,只有非核心線程,非核心線程的數量值為Integer.MAX_VALUE。
CachedThreadPool建立的原始碼為:

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

keepAliveTime為60L,說明空閑線程等待新任務id最長時間為60s,如果超過了60s,則該線程會終止,從而被回收。CachedThreadPool使用的隊列為SynchronousQueue,這是一個無界隊列。

(1)如果線程池中的線程處理任務的速度小於提交任務的速度,那麼線程池會不斷的建立新線程來處理任務,那麼過多的建立線程會耗盡CPU和記憶體資源。
(2)當線程池中的線程都處於活動狀態時,線程池會建立新的線程來處理主線程提交的任務,如果有空閑線程,則會利用空閑線程來處理任務。
(3)SynchronousQueue是一個沒有容量的阻塞隊列。每個插入操作必須等待另外一個線程的對應移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主線程提交的任務傳遞給空閑線程執行。

八、總結

熟悉了線程池的一些相關知識後,就要熟悉如何來合理配置線程池,如何選擇哪種方式的線程池。

如何合理配置線程池,就需要從任務的幾個方面來分析:

  • 任務的性質:CPU密集型、IO密集型和混合型。
  • 任務的優先順序:高、中和低。
  • 任務的執行的時間:長、中和短。
  • 任務的依賴性:是否依賴其他系統資源,如資料庫連接。

從任務的性質來說,如果是CPU密集型的任務,那麼儘可能的配置少的線程數量,例如配置N+1(N為CPU的核心數)個線程的線程池。如果是IO密集型的任務,那麼儘可能配置多的線程數,例如2*N。對於混合型的任務,如果可拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的輸送量將高於串列執行的輸送量。如果這兩個任務執行時間相差太大,則沒必要進行分解。 可以通過 Runtime.getRuntime().availableProcessors()方法獲得當前裝置 的CPU個數。

從任務的優先順序來說,可以使用優先順序隊PriorityBlockingQueue處理。優先順序高的任務會先處理,但是這樣帶來一種不太好的情況就是,優先順序低的任務可能一直得不到處理。

從執行時間不同來說,可以交給不同規模的線程池來處理,或者可以使用優先順序隊列,讓執行時間短的任務先執行。

從依賴性來說,這個主要介紹資料庫的串連,因為線程提交SQL後需要等待資料庫返回結果,等待的時間越長,則CPU空閑時間就越長,那麼線程數應該設定得越大,這樣才能更好地利用CPU。

以上三種方式的線程池,FixedThreadPool適合比較耗資源的任務,SingleThreadExecutor適合按照順序執行的任務,CachedThreadPool適合執行大量耗時較少的任務。

參考資料:
《Java並發編程的藝術》
《Java多線程編程核心技術》

Android多線程編程之線程池學習篇(一)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.