線程池的基礎與操作
一、概念
使用ThreadPoolExecutor類:包含了五個參數int
corePoolSize(線程池的基本大小),
int
maximumPoolSize(最大線程池容量),
long
keepAliveTime(存活時間),TimeUnit unit(時間單位),
BlockingQueue<Runnable> workQueue(任務隊列)
corePoolSize:當線程池剛剛創建的時候,線程池中沒用線程,當有任務創建且當前線程數小於corePoolSize時才會去創建新的線程。但是當調用prestartAllCoreThreads或prestartCoreThread方法時候才會在創建時就創建corePoolSize
maximumPoolSize:線程創建的最大數,當線程數未達到該值,並且任務隊列已滿,就可以新創建線程。
當線程達到maximumPoolSize,並且隊列也已經滿了,如果再有新任務就會采取飽和策略,默認采取AbortPolicy,用來拋出異常,表示不能再處理新的任務。
有四種策略:
- CallerRunsPolicy:只用調用者所在線程來運行任務。
- DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
- DiscardPolicy:不處理,丟棄掉。
- 當然也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務。
keepAliveTime:線程池的工作線程空閑後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率,避免重復的創建銷毀線程。
TimeUnit:時間單位:線程keepAliveTime的時間單位
BlockingQueue:任務緩存隊列,用於保存線程數達到corePoolSize之後的阻塞隊列。可以選擇以下幾種阻塞隊列:
1.ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
2.LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
3.SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
4.PriorityBlockingQueue:一個具有優先級得無限阻塞隊列。
流程圖:
二、操作
1、線程池創建:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
2、使用線程池執行任務
//execute中的參數是一個runnable接口實例 threadsPool.execute(new Runnable() { public void run(){ } } //也可以使用一個實例先實現runnable Class task implements Runnable(){ public void run(){ } } threadsPool.execute(task);
3、簡單的線程例子
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Test { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5)); for(int i=0;i<15;i++){ MyTask myTask = new MyTask(i); executor.execute(myTask); System.out.println("線程池中線程數目:"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+ executor.getQueue().size()+",已執行玩別的任務數目:"+executor.getCompletedTaskCount()); } executor.shutdown(); } } class MyTask implements Runnable { private int taskNum; public MyTask(int num) { this.taskNum = num; } @Override public void run() { System.out.println("正在執行task "+taskNum); try { Thread.currentThread().sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("task "+taskNum+"執行完畢"); } }
4、線程執行任務有兩個方法:submit和execute
不同點:
1、submit接受的參數可以是callable接口實例,runnable接口實例;而execute接受的參數只有runnable接口實例
2、對待異常的處理不同,execute和正常的一樣拋出異常,而submit如果不是用get方法的時候就會吃掉異常,返回的future對象使用get方法後才會打印簡略異常。
3、submit的返回的future對象使用get方法由於需要等待接收任務的返回值所以會阻塞線程池,同步阻塞執行任務,任務執行成功,就返回一個null。
三、線程池的合理配置考慮
根據任務的不同進行分門別類的處理:
1、任務的性質不同:io密集型任務、cpu運算密集型任務、混合型任務
2、優先級不同:高中低
3、需要時間不同的任務:短中長
4、是否有依賴
1、性質不同: 如果是cpu運算密集型的任務的話,由於運算快,線程的利用率更高,所以盡可能少的設置最大線程數,cpu+1個為好
如果是io存儲密集型任務的話,由於存儲時io較為耗時,線程利用率更低,所以設置2*task個線程為好。
2、時間不同和優先級不同以及有否依賴都可以通過使用優先級隊列來解決
時間短的優先級大些先完成,優先級高的也將其設置為高優先級,依賴底層的也設置高優先級
風險:建議使用有界隊列,如果使用無界隊列,一旦任務耗時嚴重,線程達到了線程corepoolsize,就會往任務隊列裏存放,如果一直存放的話會造成內存溢出oom異常。
線程池的基礎與操作