一:使用線程池的原因
在android開發中經常會使用多線程異步來處理相關任務,而如果用傳統的newThread來創建一個子線程進行處理,會造成一些嚴重的問題:
1:在任務衆多的情況下,系統要爲每一個任務創建一個線程,而任務執行完畢後會銷燬每一個線程,所以會造成線程頻繁地創建與銷燬。
2:多個線程頻繁地創建會佔用大量的資源,並且在資源競爭的時候就容易出現問題,同時這麼多的線程缺乏一個統一的管理,容易造成界面的卡頓。
3:多個線程頻繁地銷燬,會頻繁地調用GC機制,這會使性能降低,又非常耗時。
總而言之:頻繁地爲每一個任務創建一個線程,缺乏統一管理,降低性能,並且容易出現問題。
爲了解決這些問題,就要用到今天的主角——線程池.
線程池使用的好處:
1:對多個線程進行統一地管理,避免資源競爭中出現的問題。
2:(重點):對線程進行復用,線程在執行完任務後不會立刻銷燬,而會等待另外的任務,這樣就不會頻繁地創建、銷燬線程和調用GC。
3:JAVA提供了一套完整的ExecutorService線程池創建的api,可創建多種功能不一的線程池,使用起來很方便。
二:幾種常見的線程池
1:ThreadPoolExecutor 創建基本線程池
創建線程池,主要是利用ThreadPoolExecutor這個類,而這個類有幾種構造方法,其中參數最多的一種構造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
...
}
corePoolSize: 該線程池中核心線程的數量。
maximumPoolSize:該線程池中最大線程數量。(區別於corePoolSize)
keepAliveTime:從字面上就可以理解,是非核心線程空閒時要等待下一個任務到來的時間,當任務很多,每個任務執行時間很短的情況下調大該值有助於提高線程利用率。注意:當allowCoreThreadTimeOut屬性設爲true時,該屬性也可用於核心線程。
unit:上面時間屬性的單位
workQueue:任務隊列,後面詳述。
threadFactory:線程工廠,可用於設置線程名字等等,一般無須設置該參數。
設置好幾個參數就可以創建一個基本的線程池,而之後的各種線程池都是在這種基本線程池的基礎上延伸的。
下面貼個自己寫的demo來熟悉具體的使用並且加深影響:
//創建基本線程池
final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,1,TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(100));
設置一個按鈕mThreadPoolExecute,並在點擊事件中使用線程池
/**
* 基本線程池使用
*/
mThreadPoolExecute.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for(int i = 0;i<30;i++){
final int finali = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d("Thread", "run: "+finali);
Log.d("當前線程:",Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
threadPoolExecutor.execute(runnable);
}
}
});
結果會每2s打印三個日誌。
具體過程:
1.execute一個線程之後,如果線程池中的線程數未達到核心線程數,則會立馬啓用一個核心線程去執行。
2.execute一個線程之後,如果線程池中的線程數已經達到核心線程數,且workQueue未滿,則將新線程放入workQueue中等待執行。
3.execute一個線程之後,如果線程池中的線程數已經達到核心線程數但未超過非核心線程數,且workQueue已滿,則開啓一個非核心線程來執行任務。
4.execute一個線程之後,如果線程池中的線程數已經超過非核心線程數,則拒絕執行該任務,採取飽和策略,並拋出RejectedExecutionException異常。
demo中設置的任務隊列長度爲100,所以不會開啓額外的5-3=2個非核心線程,如果將任務隊列設爲25,則前三個任務被核心線程執行,剩下的30-3=27個任務進入隊列會滿,此時會開啓2個非核心線程來執行剩下的兩個任務。
//新開啓了thread-4與thread-5執行剩下的超出隊列的兩個任務28和29
2019-03-28 15:54:07.879 22284-22618/com.example.threadpooltest D/Thread:: 1
2019-03-28 15:54:07.879 22284-22617/com.example.threadpooltest D/Thread:: 0
2019-03-28 15:54:07.879 22284-22617/com.example.threadpooltest D/當前線程:: pool-1-thread-1
2019-03-28 15:54:07.879 22284-22618/com.example.threadpooltest D/當前線程:: pool-1-thread-2
2019-03-28 15:54:07.880 22284-22619/com.example.threadpooltest D/Thread:: 2
2019-03-28 15:54:07.880 22284-22619/com.example.threadpooltest D/當前線程:: pool-1-thread-3
2019-03-28 15:54:07.881 22284-22620/com.example.threadpooltest D/Thread:: 28
2019-03-28 15:54:07.881 22284-22620/com.example.threadpooltest D/當前線程:: pool-1-thread-4
2019-03-28 15:54:07.881 22284-22621/com.example.threadpooltest D/Thread:: 29
2019-03-28 15:54:07.881 22284-22621/com.example.threadpooltest D/當前線程:: pool-1-thread-5
疑問:每個for循環裏都有一個sleep(2000),爲何會每隔2s打印三個任務?
原因:因爲一開始的時候只是聲明runnable對象並且重寫run()方法,並沒有運行,而後execute(runnable) 纔會sleep,又因爲一開始創建線程池的時候聲明的核心線程數爲3,所以會首先開啓三個核心線程,然後執行各自的run方法,雖然有先後順序,但這之間的間隔很短,所以2s後同時打印3個任務。
2:FixedThreadPool (可重用固定線程數)
Executors類中的創建方法:
FixedThreadPool創建
特點:參數爲核心線程數,只有核心線程,無非核心線程,並且阻塞隊列無界。
demo代碼:
創建:
//創建fixed線程池
final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
使用:
/**
* fixed線程池
*/
mFixedPoolThread.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for(int i = 0;i<30;i++){
final int finali = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d("Thread", "run: "+finali);
Log.d("當前線程:",Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
fixedThreadPool.execute(runnable);
}
}
});
結果爲每2s打印5次任務,跟上面的基礎線程池類似。
3:CachedThreadPool (按需創建)
Executors類中的創建方法:
CachedThreadPool創建
特點:沒有核心線程,只有非核心線程,並且每個非核心線程空閒等待的時間爲60s,採用SynchronousQueue隊列。
demo代碼:
創建:
//創建Cached線程池
final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
使用:
/**
* cached線程池
*/
mCachedPoolThread.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for(int i = 0;i<30;i++){
final int finali = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d("Thread", "run: "+finali);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
cachedThreadPool.execute(runnable);
}
}
});
結果:過2s後直接打印30個任務
結果分析:
- 因爲沒有核心線程,其他全爲非核心線程,SynchronousQueue是不存儲元素的,每次插入操作必須伴隨一個移除操作,一個移除操作也要伴隨一個插入操作。
- 當一個任務執行時,先用SynchronousQueue的offer提交任務,如果線程池中有線程空閒,則調用SynchronousQueue的poll方法來移除任務並交給線程處理;如果沒有線程空閒,則開啓一個新的非核心線程來處理任務。
- 由於maximumPoolSize是無界的,所以如果線程處理任務速度小於提交任務的速度,則會不斷地創建新的線程,這時需要注意不要過度創建,應採取措施調整雙方速度,不然線程創建太多會影響性能。
- 從其特點可以看出,CachedThreadPool適用於有大量需要立即執行的耗時少的任務的情況。
4:SingleThreadPool(單個核線的fixed)
創建方法:
SingleThreadPool創建
創建:
//創建Single線程池
final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
使用:
/**
* single線程池
*/
mSinglePoolExecute.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for(int i = 0;i<30;i++){
final int finali = i;x
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
Log.d("Thread", "run: "+finali);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
singleThreadExecutor.execute(runnable);
}
}
});
結果:每2s打印一個任務,由於只有一個核心線程,當被佔用時,其他的任務需要進入隊列等待。
5:ScheduledThreadPool(定時延時執行)
創建方法:
ScheduledThreadPool創建1
ScheduledThreadPool創建2
創建:
//創建Scheduled線程池
final ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
使用:
/**
* scheduled線程池
*/
mScheduledTheadPool.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.d("Thread", "This task is delayed to execute");
}
};
scheduledThreadPool.schedule(runnable,10,TimeUnit.SECONDS);//延遲啓動任務
//延遲5s後啓動,每1s執行一次 scheduledThreadPool.scheduleAtFixedRate(runnable,5,1,TimeUnit.SECONDS);
//啓動後第一次延遲5s執行,後面延遲1s執行 scheduledThreadPool.scheduleWithFixedDelay(runnable,5,1,TimeUnit.SECONDS);
}
});
結果如代碼所述。
6:自定義的PriorityThreadPool(隊列中有優先級比較的線程池)
創建:
//創建自定義線程池(優先級線程)
final ExecutorService priorityThreadPool = new ThreadPoolExecutor(3,3,0, TimeUnit.SECONDS,new PriorityBlockingQueue<Runnable>());
自定義Runnable,繼承Comparable接口:
public abstract class PriorityRunnable implements Runnable,Comparable<PriorityRunnable> {
private int priority;
public PriorityRunnable(int priority){
if(priority <0) {
throw new IllegalArgumentException();
}
this.priority = priority;
}
public int getPriority() {
return priority;
}
@Override
public int compareTo(@NonNull PriorityRunnable another) {
int me = this.priority;
int anotherPri=another.getPriority();
return me == anotherPri ? 0 : me < anotherPri ? 1 : -1;
}
@Override
public void run() {
doSomeThing();
}
protected abstract void doSomeThing();
}
利用抽象類繼承Comparable接口重寫其中的compareTo方法來比較優先級。
使用:
mMyPriorityTheadPool.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
for(int i = 0;i<30;i++){
final int priority = i;
priorityThreadPool.execute(new PriorityRunnable(priority) {
@Override
protected void doSomeThing() {
Log.d("MainActivity", "優先級爲 "+priority+" 的任務被執行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
});
結果:前三個任務被創建的三個核心線程執行,之後的27個任務進入隊列並且調用compareTo方法進行排序,之後打印出來的是經過排序後從大到小的順序。
三:JAVA中的阻塞隊列
由於上面的構造方法涉及到了阻塞隊列,所以補充一些阻塞隊列的知識。
阻塞隊列:我的理解是,生產者——消費者,生產者往隊列裏放元素,消費者取,如果隊列裏沒有元素,消費者線程取則阻塞,如果隊列裏元素滿了,則生產者線程阻塞。
常見的阻塞隊列有下列7種:
ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
具體情況具體分析,選擇合適的隊列。
由於只是補充部分,所以具體使用及實現原理請百度。
四:各個線程池總結及適用場景
newCachedThreadPool:
底層:返回ThreadPoolExecutor實例,corePoolSize爲0;maximumPoolSize爲Integer.MAX_VALUE;keepAliveTime爲60L;unit爲TimeUnit.SECONDS;workQueue爲SynchronousQueue(同步隊列)
通俗:當有新任務到來,則插入到SynchronousQueue中,由於SynchronousQueue是同步隊列,因此會在池中尋找可用線程來執行,若有可以線程則執行,若沒有可用線程則創建一個線程來執行該任務;若池中線程空閒時間超過指定大小,則該線程會被銷燬。
適用:執行很多短期異步的小程序或者負載較輕的服務器
newFixedThreadPool:
底層:返回ThreadPoolExecutor實例,接收參數爲所設定線程數量nThread,corePoolSize爲nThread,maximumPoolSize爲nThread;keepAliveTime爲0L(不限時);unit爲:TimeUnit.MILLISECONDS;WorkQueue爲:new LinkedBlockingQueue<Runnable>() 無解阻塞隊列
通俗:創建可容納固定數量線程的池子,每隔線程的存活時間是無限的,當池子滿了就不再添加線程了;如果池中的所有線程均在繁忙狀態,對於新任務會進入阻塞隊列中(無界的阻塞隊列)
適用:執行長期的任務,性能好很多
newSingleThreadExecutor:
底層:FinalizableDelegatedExecutorService包裝的ThreadPoolExecutor實例,corePoolSize爲1;maximumPoolSize爲1;keepAliveTime爲0L;unit爲:TimeUnit.MILLISECONDS;workQueue爲:new LinkedBlockingQueue<Runnable>() 無解阻塞隊列
通俗:創建只有一個線程的線程池,且線程的存活時間是無限的;當該線程正繁忙時,對於新任務會進入阻塞隊列中(無界的阻塞隊列)
適用:一個任務一個任務執行的場景
NewScheduledThreadPool:
底層:創建ScheduledThreadPoolExecutor實例,corePoolSize爲傳遞來的參數,maximumPoolSize爲Integer.MAX_VALUE;keepAliveTime爲0;unit爲:TimeUnit.NANOSECONDS;workQueue爲:new DelayedWorkQueue() 一個按超時時間升序排序的隊列
通俗:創建一個固定大小的線程池,線程池內線程存活時間無限制,線程池可以支持定時及週期性任務執行,如果所有線程均處於繁忙狀態,對於新任務會進入DelayedWorkQueue隊列中,這是一種按照超時時間排序的隊列結構
適用:週期性執行任務的場景
五:線程池其它方法:
1.shutDown() 關閉線程池,不影響已經提交的任務
2.shutDownNow() 關閉線程池,並嘗試去終止正在執行的線程
3.allowCoreThreadTimeOut(boolean value) 允許核心線程閒置超時時被回收
4.submit 一般情況下我們使用execute來提交任務,但是有時候可能也會用到submit,使用submit的好處是submit有返回值。
5.beforeExecute() - 任務執行前執行的方法
6.afterExecute() -任務執行結束後執行的方法
7.terminated() -線程池關閉後執行的方法
參考出處鏈接:
Android性能優化之使用線程池處理異步任務
Android開發之線程池使用總結
線程池的種類,區別和使用場景
Android 進階之光 ——劉望舒 (一本好書,推薦)
作者:無問o
鏈接:https://www.jianshu.com/p/7b2da1d94b42
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。