Java 執行緒池的建立過程分析
最近在改進專案的併發功能,但開發起來磕磕碰碰的。看了好多資料,總算加深了認識。於是打算配合檢視原始碼,總結併發程式設計的原理。
準備從用得最多的執行緒池開始,圍繞建立、執行、關閉認識執行緒池整個生命週期的實現原理。後續再研究原子變數、併發容器、阻塞佇列、同步工具、鎖等等主題。java.util.concurrent裡的併發工具用起來不難,但不能僅僅會用,我們要read the fucking source code,哈哈。順便說聲,我用的JDK是1.8。
Executor框架
Executor是一套執行緒池管理框架,介面裡只有一個方法execute,執行Runnable任務。ExecutorService介面擴充套件了Executor,新增了執行緒生命週期的管理,提供任務終止、返回任務結果等方法。AbstractExecutorService實現了ExecutorService,提供例如submit方法的預設實現邏輯。
然後到今天的主題ThreadPoolExecutor,繼承了AbstractExecutorService,提供執行緒池的具體實現。
構造方法
下面是ThreadPoolExecutor最普通的建構函式,最多有七個引數。具體程式碼不貼了,只是一些引數校驗和設定的語句。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { }
corePoolSize是執行緒池的目標大小,即是執行緒池剛剛建立起來,還沒有任務要執行時的大小。maximumPoolSize是執行緒池的最大上限。keepAliveTime是執行緒的存活時間,當執行緒池內的執行緒數量大於corePoolSize,超出存活時間的空閒執行緒就會被回收。unit就不用說了,剩下的三個引數看後文的分析。
預設的定製執行緒池
ThreadPoolExecutor預設了一些已經定製好的執行緒池,由Executors裡的工廠方法建立。下面分析newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool的建立引數。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
newFixedThreadPool的corePoolSize和maximumPoolSize都設定為傳入的固定數量,keepAliveTim設定為0。執行緒池建立後,執行緒數量將會固定不變,適合需要執行緒很穩定的場合。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
newSingleThreadExecutor是執行緒數量固定為1的newFixedThreadPool版本,保證池內的任務序列。注意到返回的是FinalizableDelegatedExecutorService,來看看原始碼:
static class FinalizableDelegatedExecutorService extends DelegatedExecutorService { FinalizableDelegatedExecutorService(ExecutorService executor) { super(executor); } protected void finalize() { super.shutdown(); } }
FinalizableDelegatedExecutorService繼承了DelegatedExecutorService,僅僅在gc時增加關閉執行緒池的操作,再來看看DelegatedExecutorService的原始碼:
static class DelegatedExecutorService extends AbstractExecutorService { private final ExecutorService e; DelegatedExecutorService(ExecutorService executor) { e = executor; } public void execute(Runnable command) { e.execute(command); } public void shutdown() { e.shutdown(); } public List<Runnable> shutdownNow() { return e.shutdownNow(); } public boolean isShutdown() { return e.isShutdown(); } public boolean isTerminated() { return e.isTerminated(); } //... }
程式碼很簡單,DelegatedExecutorService包裝了ExecutorService,使其只暴露出ExecutorService的方法,因此不能再配置執行緒池的引數。本來,執行緒池建立的引數是可以調整的,ThreadPoolExecutor提供了set方法。使用newSingleThreadExecutor目的是生成單執行緒序列的執行緒池,如果還能配置執行緒池大小,那就沒意思了。
Executors還提供了unconfigurableExecutorService方法,將普通執行緒池包裝成不可配置的執行緒池。如果不想執行緒池被不明所以的後人修改,可以呼叫這個方法。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
newCachedThreadPool生成一個會快取的執行緒池,執行緒數量可以從0到Integer.MAX_VALUE,超時時間為1分鐘。執行緒池用起來的效果是:如果有空閒執行緒,會複用執行緒;如果沒有空閒執行緒,會新建執行緒;如果執行緒空閒超過1分鐘,將會被回收。
newScheduledThreadPool
newScheduledThreadPool將會建立一個可定時執行任務的執行緒池。這個不打算在本文展開,後續會另開文章細講。
等待佇列
newCachedThreadPool的執行緒上限幾乎等同於無限,但系統資源是有限的,任務的處理速度總有可能比不上任務的提交速度。因此,可以為ThreadPoolExecutor提供一個阻塞佇列來儲存因執行緒不足而等待的Runnable任務,這就是BlockingQueue。
JDK為BlockingQueue提供了幾種實現方式,常用的有:
- ArrayBlockingQueue:陣列結構的阻塞佇列
- LinkedBlockingQueue:連結串列結構的阻塞佇列
- PriorityBlockingQueue:有優先順序的阻塞佇列
- SynchronousQueue:不會儲存元素的阻塞佇列
newFixedThreadPool和newSingleThreadExecutor在預設情況下使用一個無界的LinkedBlockingQueue。要注意的是,如果任務一直提交,但執行緒池又不能及時處理,等待佇列將會無限制地加長,系統資源總會有消耗殆盡的一刻。所以,推薦使用有界的等待佇列,避免資源耗盡。但解決一個問題,又會帶來新問題:佇列填滿之後,再來新任務,這個時候怎麼辦?後文會介紹如何處理佇列飽和。
newCachedThreadPool使用的SynchronousQueue十分有趣,看名稱是個佇列,但它卻不能儲存元素。要將一個任務放進佇列,必須有另一個執行緒去接收這個任務,一個進就有一個出,佇列不會儲存任何東西。因此,SynchronousQueue是一種移交機制,不能算是佇列。newCachedThreadPool生成的是一個沒有上限的執行緒池,理論上提交多少任務都可以,使用SynchronousQueue作為等待佇列正合適。
飽和策略
當有界的等待佇列滿了之後,就需要用到飽和策略去處理,ThreadPoolExecutor的飽和策略通過傳入RejectedExecutionHandler來實現。如果沒有為建構函式傳入,將會使用預設的defaultHandler。
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }
AbortPolicy是預設的實現,直接丟擲一個RejectedExecutionException異常,讓呼叫者自己處理。除此之外,還有幾種飽和策略,來看一下:
public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } }
DiscardPolicy的rejectedExecution直接是空方法,什麼也不幹。如果佇列滿了,後續的任務都拋棄掉。
public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } }
DiscardOldestPolicy會將等待佇列裡最舊的任務踢走,讓新任務得以執行。
public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } }
最後一種飽和策略是CallerRunsPolicy,它既不拋棄新任務,也不拋棄舊任務,而是直接在當前執行緒執行這個任務。當前執行緒一般就是主執行緒啊,讓主執行緒執行任務,說不定就阻塞了。如果不是想清楚了整套方案,還是少用這種策略為妙。
ThreadFactory
每當執行緒池需要建立一個新執行緒,都是通過執行緒工廠獲取。如果不為ThreadPoolExecutor設定一個執行緒工廠,就會使用預設的defaultThreadFactory:
public static ThreadFactory defaultThreadFactory() { return new DefaultThreadFactory(); }
static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
平時列印執行緒池裡執行緒的name時,會輸出形如pool-1-thread-1之類的名稱,就是在這裡設定的。這個預設的執行緒工廠,建立的執行緒是普通的非守護執行緒,如果需要定製,實現ThreadFactory後傳給ThreadPoolExecutor即可。
不看程式碼不總結不會知道,光是執行緒池的建立就可以引出很多學問。別看平時建立執行緒池是一句程式碼的事,其實ThreadPoolExecutor提供了很靈活的定製方法。
歡迎留言和轉發,下一篇打算分析執行緒池如何執行任務。
相關文章
- 執行緒池建立執行緒的過程執行緒
- Java執行緒池的增長過程Java執行緒
- java--執行緒池--建立執行緒池的幾種方式與執行緒池操作詳解Java執行緒
- Java 執行緒池執行原理分析Java執行緒
- 執行緒的建立及執行緒池執行緒
- Java排程執行緒池ScheduledThreadPoolExecutor原始碼分析Java執行緒thread原始碼
- Java執行緒池原理及分析Java執行緒
- Java執行緒池二:執行緒池原理Java執行緒
- Java面試必問之執行緒池的建立使用、執行緒池的核心引數、執行緒池的底層工作原理Java面試執行緒
- Java 四種執行緒池的用法分析Java執行緒
- 詳解Java執行緒池的ctl(執行緒池控制狀態)【原始碼分析】Java執行緒原始碼
- Java多執行緒——執行緒池Java執行緒
- Java多執行緒-執行緒池的使用Java執行緒
- Informix 執行緒sleep 分析過程ORM執行緒
- Java執行緒池Java執行緒
- java 執行緒池Java執行緒
- Java 中執行緒池的7種建立方式!Java執行緒
- java執行緒池趣味事:這不是執行緒池Java執行緒
- 執行緒池的建立和使用,執行緒池原始碼初探(篇一)執行緒原始碼
- java多執行緒9:執行緒池Java執行緒
- Java多執行緒18:執行緒池Java執行緒
- JAVA執行緒池的使用Java執行緒
- 搞懂Java執行緒池Java執行緒
- Java Executors(執行緒池)Java執行緒
- Java執行緒池(Executor)Java執行緒
- 執行緒池的五種狀態及建立執行緒池的幾種方式執行緒
- Java執行緒池一:執行緒基礎Java執行緒
- 【Java】【多執行緒】執行緒池簡述Java執行緒
- java多執行緒系列之執行緒池Java執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 使用CreateThreadPool建立執行緒池thread執行緒
- Java通過Executors提供四種執行緒池Java執行緒
- SpringBoot執行緒池和Java執行緒池的實現原理Spring Boot執行緒Java
- 詳解執行緒池的作用及Java中如何使用執行緒池執行緒Java
- Spring 非同步執行緒池、排程任務執行緒池配置Spring非同步執行緒
- Java中的執行緒池用過吧?來說說你是怎麼理解執行緒池吧?Java執行緒
- Java 執行緒池(ThreadPoolExecutor)原理分析與使用Java執行緒thread
- Java執行緒池的那些事Java執行緒