首先要知道線程池是用來幹嘛的,線程池是通過複用線程達到最大利用資源的。
線程池的關鍵參數
線程池的構造方法如下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
簡單解釋一下:
corePoolSize : 核心線程數,默認情況下不會退出;
maximumPoolSize: 最大線程數,這個數字是包括核心線程數的,最大線程數 = 核心線程數+非核心線程數;
long keepAliveTime:線程保持存活的時間,核心線程也可以設置,但是默認情況下不會退出;非核心線程數這個參數默認生效;
TimeUnit unit :keepAliveTime 的時間單位,時分秒等;
workQueue : 工作隊列,是個阻塞隊列;
threadFactory : 線程工廠,通常用來設置線程的名字和優先級等;
handler :當線程池處理不了提交的任務時,需要告訴線程池要怎麼處理,例如拋異常等。
線程池模型
網上有很多講解線程池的圖片、代碼講解,但是看起來依然很費力,我把源碼大體線程複用流程寫了個僞代碼(核心線程不停的默認情況),可以對比源碼理思路,這裏不貼了:
void execute(Runnable r){
if(當前線程數<core ){
New Thread(new Runnable(){
Public void run(){
While(r!=null || r = takeFromQueue()!= null){
r.run();
r = null;
}
}
})
}else if(!queue.isFull()){
queque.addTask(r);
}else if(當前線程數<max ){
New Thread(new Runnbale(){
Public void run(){
While(r != null || r = pollAllowTimeout(超時時間)){
r.run();
R = null;
}
}
})
}else{
拒絕策略;
}
}
僞代碼中線程裏面的操作很像,但是爲了好理解分開寫了。執行順序:
1、如果有任務來了,沒有到達核心線程數,會立即新建一個線程執行,並且線程生命週期不會結束(terminal狀態),因爲run()方法沒有走完,這個任務執行完後(r = null),線程會繼續從阻塞隊列裏拿任務,拿不到會一直阻塞(take),默認不會停。
2、超過核心數,但是阻塞隊列沒有滿,會把任務放在阻塞隊列裏;
3、阻塞隊列滿了,但是沒有達到最大核心數,會新建一個線程,立馬執行這個任務,執行完後從隊列取任務,但是這時候是poll,是有超時退出的取,這個超時時間就是keepAliveTime。
5、隊列滿了,線程數也達到最大值了,線程池不知道怎麼處理,用handler的策略來拒絕任務。
線程池的原理
1、根據上面分析,核心線程默認不會停的,線程的生命週期在run()方法執行完結束,首先會在while中循環,提交的任務執行完後還會從隊列取,即使隊列爲空,也會阻塞在那等待;–複用線程
2、核心任務(沒達到核心線程數時提交的任務)是不會加到隊列裏的,非核心任務纔會加到隊列裏;用隊列控制吞吐,是常用的設計思想之一。–控制吞吐
再來看看我們常用的幾種線程池:
CachedThreadPool :
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心線程數爲0,最大線程數無界(Integer.MAX_VALUE),60s後非核心線程退出,使用的隊列是同步隊列SynchronousQueue,SynchronousQueue是個無容量隊列,所以不能通過peek方法獲取頭部元素;也不能單獨插入元素,put和take是成對出現的,put一個元素,一定要等另一端take掉。所以CachedThreadPool一旦有任務,會立即新建一個非核心線程執行,由於是線程數無上限,所以一旦任務多,就會不停地新建線程,60s後還沒執行完,就會超時退出。另一個角度,任務在小於且接近60s的時間裏結束,那麼可能會有大量的線程在同時進行,佔用CPU,這種情況總結就是**“提交速度大於任務執行速度”,所以CachedThreadPool適合提交頻繁,且執行時間短的任務**。
FixedThreadPool:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
核心線程數和最大線程數是一樣的,也就是隻有n個核心線程,而LinkedBlockingQueue是無界的隊列(容量爲Integer.MAX_VALUE),也就是不會滿,也就不會產生非核心線程數,第一個任務結束後,核心線程會一直從隊列裏取任務不退出。
SingleThreadExecutor:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
核心線程數和最大線程數都是1,是FixedThreadPool的特例(n = 1),不同的是,由於只有一個核心線程,保證了所有任務都是在一個線程裏有序執行。
Android線程池和內存優化
線程池和內存有什麼關係?
1、核心線程的鍋。由於核心線程默認不退出的特點,核心線程指向的對象一直都是運行中的,如果核心線程開的很大,那麼這些線程對象在線程池使用shutdown之前,都是佔內存的。
再看下面一個例子:
Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(@NonNull final Runnable r) {
new Thread(){
@Override
public void run() {
r.run();
}
};
return null;
}
});
factory裏的線程是匿名內部類,一旦這段代碼放在Activity裏,那麼Thread對象會持有Activity的引用,而且是個強引用,那麼外部的activity就一定會內存泄漏。
解決辦法:
- ThreadFactory 通過聲明爲靜態內部類;
- 如果在Activity中的線程池,有必要的話請考慮在onDestory的時候線程池shutdown。
2、阻塞隊列的鍋。像SingleThreadPool和FixThreadPool這兩種線程池,隊列大小是無界的,一旦線程處理的速度慢,而任務提交的快,就會有大量的任務堆積在隊列裏,從而在執行完之前造成一段時間的內存泄漏。
解決辦法:合理設置線程池大小。