編輯:關於Android編程
Android應用開發中多線程編程應用比較廣泛,而應用比較多的是ThreadPoolExecutor,AsyncTask,IntentService,HandlerThread,AsyncTaskLoader等,為了更詳細的分析每一種實現方式,將單獨成篇分析。後續篇章中可能涉及到線程池的知識,特此本篇分析為何使用線程池,如何使用線程池以及線程池的使用原理。
進程代表一個運行中的程序,一個運行中的Android應用程序就是一個進程。從操作系統的方面來說,線程是進程中可以獨立執行的子任務。一個進程可以有多個線程,同一個進程中的線程可以共享進程中的資源。從JVM的方面來說,線程是進程中的一個組件,是執行java代碼的最小單位。
在Android應用開發過程中,如果需要處理異步或並發任務時可以使用線程池,使用線程池可以有以下好處:
1、降低資源消耗:線程的創建和銷毀都需要消耗資源,重復利用線程可以避免過度的消耗資源。
2、提高響應速度:當有任務需要執行時,可以不用重新創建線程就能開始執行任務。
3、提高線程的管理性:過多的創建線程會降低系統的穩定性,使用線程池可以統一分配,調優和監控。
Thread Pool模式的原理是使用隊列對待處理的任務進行緩存,並復用一定數量的工作者線程從隊列中取出任務來執行。其本質是使用有限的資源來處理無限的任務。
Thread Pool模式最核心的類是ThreadPoolExecutor,它是線程池的實現類。使用Executors可以創建三種類型的ThreadPoolExecutor類,主要是以下三種類型:
(1)FixedThreadPool
(2)SingleThreadExecutor
(3)CachedThreadPool
Executor接口提供一種將任務提交與每個任務將如何運行的機制(包括線程使用的細節、調度等)分離開來的方法。
Executor框架中主要包括:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runable接口、Callable接口和Executors。
下圖是Executor框架的類圖。
==首先主線程創建任務:==任務的對象可以通過實現Runnable接口或者Callable接口實現。而Runnable可以通過Executors.callable(Runnable runnable)或者Executors.callable(Runnable runnable, Object result)方法來轉換封裝為Callable對象。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPj09xuS688rH1rTQ0MjOzvGjuj091rTQ0MjOzvG1xLe9yr3T0MG91tajrNK71tbKx2V4ZWN1dCgpt723qCzWtNDQzOG9u7XEUnVubmFibGW21M/zo6xFeGVjdXRvclNlcnZpY2UuZXhlY3V0ZShSdW5uYWJsZSBydW5uYWJsZSk7we3N4tK71tbKx3N1Ym1pdCgpt723qKOsv8nS1Na00NDM4b27tcRSdW5uYWJsZbbUz/OjrEV4ZWN1dG9yU2VydmljZS5zdWJtaXQoUnVubmFibGUgcnVubmFibGUpLLvy1d/Kx9a00NDM4b27tcRDYWxsYWJsZbbUz/OjrEV4ZWN1dG9yU2VydmljZS5zdWJtaXQoQ2FsbGFibGUgY2FsbGFibGUpoaM8L3A+DQo8cD49Pcihz/vIzs7xo7o9Pb/J0tTRodTxyrnTw0ZldHVyZVRhc2suY2FuY2VsKGJvb2xlYW4gZmxhZynIoc/7yM7O8aGjPC9wPg0KPHA+PT252LHVIEV4ZWN1dG9yU2VydmljZaO6PT3V4r2rtbzWwsbkvty++NDCyM7O8aGj09DBvdbWt73KvcC0udix1SBFeGVjdXRvclNlcnZpY2Who3NodXRkb3duKCkgt723qNTa1tXWucew1MrQ7da00NDS1MewzOG9u7XEyM7O8aOstvggc2h1dGRvd25Ob3coKSC3vbeo1+jWubXItP3Izs7xxvS2r7KiytTNvM2j1rm1scew1f3U2ta00NC1xMjOzvGho9Ta1tXWucqxo6zWtNDQs8zQ8sO709DIzs7x1NrWtNDQo6zSssO709DIzs7x1Nq1yLT91rTQ0KOssqLH0s7et6jM4b270MLIzs7xoaM8L3A+DQo8cD7XotLio7rTprjDudix1c60yrnTw7XEIEV4ZWN1dG9yU2VydmljZSDS1NTK0O272MrVxuTXytS0oaM8L3A+DQo8cD7PwsHQt723qLfWwb249r3Xts652LHVIEV4ZWN1dG9yU2VydmljZaGjtdrSu73Xts6199PDIHNodXRkb3duIL7cvvi0q8jryM7O8aOsyLu687X308Mgc2h1dGRvd25Ob3ejqMjn09Cx2NKqo6nIoc/7y/nT0NLFwfS1xMjOzvGjujwvcD4NCjxwcmUgY2xhc3M9"brush:java;">
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();
}
}
當向一個線程池中添加一個任務時,線程池是如何工作的?下面根據源代碼來分析其中的原理:
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通過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是通過使用Executors的newSingleThreadExecutor方法來創建的,以無界隊列方式來運行該線程。這個線程池中內部只有一個核心線程,而且沒有非核心線程。SingleThreadExecutor的源代碼實現如下:
public static ExecutorService newSingleThreadExecutor(){
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
其中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是使用Executors中的newCachedThreadPool()方法創建,它是一種線程數量不固定的線程池,沒有核心線程,只有非核心線程,非核心線程的數量值為Integer.MAX_VALUE。
CachedThreadPool創建的源代碼為:
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue(),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多線程編程核心技術》
第7節 播放音樂音樂播放列表也准備好了,我們來播放音樂吧。完成後效果如下,實現音樂的播放,我們需要播放界面和音樂服務兩個方面的合作。7.1 MusicService前面我
之前寫過一篇關於Android 繼承DialogFragment彈出dialog對話框一,這次是在上次的基礎上修改了一些東西,就是怎樣在DialogFragment中獲取
Android——滑動監聽RecyclerView線性流+左右劃刪除+上下移動