深入淺出JAVA線程池使用原理1
前言:
Java中的線程池是並發框架中運用最多的,幾乎所有需要異步或並發執行任務的程序都可以使用線程池,線程池主要有三個好處:
1、降低資源消耗:可以重復使用已經創建的線程降低線程創建和銷毀帶來的消耗
2、提高響應速度:執行任務時,不需要等待線程的創建就可以直接執行任務
3、提高線程的可管理性:線程是稀缺資源,如果無限制地創建不僅會消耗系統資源,還會降低系統的穩定性,線程池可以對線程進行統一分配、調優和監控
一、線程池的實現原理
在了解線程池實現原理之前,先了解線程池的一些元素
1.核心線程池
線程池有一個核心線程池,核心線程池的大小在線程池創建時設定,默認是有任務提交時會創建線程來執行,也可以調用線程池的prestartAllCoreThreads來提前創建並啟動所有的基本線程。
2.任務隊列
當核心線程池中的線程數超過設置的的大小時,再有新的任務提交則會先將任務當道隊列中,等待核心線程池中的線程執行當前的任務之後,再到隊列中獲取任務來執行
3.飽和策略
當線程池已經處於飽和狀態,無法再分配線程給新的任務時,會采用飽和策略拒絕新的任務
1.1.線程池的工作流程
線程池的整體工作流程如下圖示
當有新任務提交到線程池時,線程池的處理流程如下:
1.判斷核心線程池是否已滿,如果沒滿,則創建新線程來執行此任務(即使當前有空閑的線程也會直接創建,而不是使用空閑的線程來執行),直到核心線程池中的線程數達到了設置的大小之後就不再創建;
如果核心線程池已經滿了,則進入下一階段的判斷
2.判斷工作隊列是否已經滿了,如果工作隊列沒有滿,則將任務暫時存放到工作隊列中,等待核心線程池中的線程空閑下來再來獲取任務執行(核心線程池中的線程執行完任務之後會循環從工作隊列中取任務來執行);如果隊列也滿了,則進入下一階段的判斷
3.判斷線程池的是否已滿(線程池除了核心線程池,還設置了線程池的最大線程大小,即使核心線程池滿了,還是可以再創建線程),如果線程池中工作的線程沒有達到最大值,則創建新線程來執行任務;
如果線程池也已經滿了, 則按照線程池的飽和策略來處理任務(具體怎麽處理任務按具體的飽和策略來執行)
1.2、線程池的創建
創建線程池可以通過ThreadPoolExecutor來創建,構造方法如下
1 public ThreadPoolExecutor(int corePoolSize, 2 int maximumPoolSize, 3 long keepAliveTime, 4 TimeUnit unit, 5 BlockingQueue<Runnable> workQueue, 6 ThreadFactory threadFactory, 7 RejectedExecutionHandler handler) { 8 if (corePoolSize < 0 || 9 maximumPoolSize <= 0 || 10 maximumPoolSize < corePoolSize || 11 keepAliveTime < 0) 12 throw new IllegalArgumentException(); 13 if (workQueue == null || threadFactory == null || handler == null) 14 throw new NullPointerException(); 15 this.corePoolSize = corePoolSize; 16 this.maximumPoolSize = maximumPoolSize; 17 this.workQueue = workQueue; 18 this.keepAliveTime = unit.toNanos(keepAliveTime); 19 this.threadFactory = threadFactory; 20 this.handler = handler; 21 }
這裏涉及到了7個參數,含義分別如下:
corePoolSize:核心線程池大小,核心線程池的用法上面已經提到,不再贅述
maximumPoolSize:線程池的最大數量,也就是線程池允許創建的最大線程數,如果線程池的工作隊列滿了,則會先判斷是否達到了最大線程數,若沒有則還可以再創建線程來執行新任務,直到線程數達到線程池的最大數量
keepAliveTime:保持線程活動時間,當核心線程池中的線程處於空閑狀態時,不會立即銷毀線程而是保持一定的活動時間來等待任務,一定程度上提高了線程的復用率。
unit:線程活動時間(keepAliveTime)的單位,可以選擇DAYS(天)、(HOURS)時、(MINUTES)分、(SECONDS)秒、(MILLSECONDS)毫秒、(MICROSECONDS)微妙、(NANOSECONDS)納秒
workQueue:工作隊列,核心線程池滿了,新任務會存放在工作隊列中等待核心線程池中的線程來獲取(後面會詳細描述)
threadFactory:線程工廠,線程池可以設置指定的線程工廠來創建新線程。如果不設置,默認是使用Executors.defaultThreadFactory()來創建
handler:飽和策略,當線程池已經滿了,則說明線程池已經處於飽和狀態無法再接受新任務了,那麽就采取飽和策略來處理新任務,默認使用AbortPolicy,表示無法處理新任務而直接拋出異常(後面會詳細描述)
1.3、線程池提交任務
線程池有兩個方法可以用來提交任務,分別是execute()和submit()
execute()方法用於提交不需要返回值的方法,所以無法判斷任務是否被線程池執行成功
submit()方法用於提交需要返回值的方法,線程池會返回一個future類型的對象,通過future對象可以判斷任務是否執行成功,並且可以通過get方法來獲取線程執行的返回值,get方法會阻塞當前線程直到任務結束,也可以給get方法設置指定時長,
則達到指定時長之後會立即返回,但是此時可能線程還沒有執行完。
1.4、關閉線程池
線程池關閉方法有shutDown方法和shutDownNow方法,原理是遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,如果線程無法響應中斷的任務就可能永遠無法終止。
shutDown方法是將線程池的狀態設置為SHUTDOWN狀態,然後中斷所有還沒有執行的任務線程
showDownNow是將線程池中的狀態設置為STOP,然後嘗試停止所有正在執行或空閑的線程,並返回等待執行任務的列表
如果需要保證任務執行完,則建議使用shutDown方法來執行關閉方法
1.5、線程池的監控
線程池有多個屬性可和方法來監控當前線程池的狀態
taskCount:線程池需要執行的任務總數量(包括等待執行和已經執行完的)
completedTaskCount:線程池已完成的任務數量
largestPoolSize:線程池運行中創建的最大線程數,可以判斷線程池運行過程中是否達到了最大線程數
getPoolSize:線程池中的線程數量
getActiveCount:獲取活動的線程數
二、線程池工作隊列和飽和策略
2.1、工作隊列 (BlockingQueue<Runnable>),由名字可以看出線程池的工作隊列是一個阻塞隊列,主要有以下四種類型:
ArrayBlockingQueue:基於數組結構的有界阻塞隊列,采用FIFO(先進先出)原則對任務進行排序
LinkedBlockingQueue:基於鏈表結構的阻塞隊列,采用FIFO(先進先出)原則對任務進行排序,吞吐量要高於ArrayBlockingQueue
SynchronousQueue:不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入一直處於阻塞狀態,吞吐量高於LinkedBlockingQueue
PriorityBlockingQueue:一個具有優先級的無限阻塞隊列
2.2、飽和策略(RejectedExecutionHandler)當線程池無法處理新任務時,就將采用飽和策略來拒絕任務的執行,主要有以下四種類型:
AbortPolicy:拒絕任務,拋出異常,也是線程池默認飽和策略
CallerRunsPolicy:拒絕任務,使用調用者的當前線程來執行此任務
DiscardOldestPolicy:丟棄工作隊列中的最近一個任務,並處理當前任務
discardPolicy:不做任何處理,直接丟棄任務
三、線程池實戰(測試案例)
先創建一個任務類,代碼如下:
1 public static class ThreadPoolTaskTest implements Runnable 2 { 3 private int taskIndex;//任務編號 4 5 public ThreadPoolTaskTest(int i) 6 { 7 taskIndex = i; 8 } 9 10 @Override 11 public void run() 12 { 13 try 14 { 15 //線程睡眠2秒 16 Thread.sleep(2000); 17 System.out.println("線程:"+Thread.currentThread().getName()+"執行任務:"+taskIndex); 18 } 19 catch (InterruptedException e) 20 { 21 e.printStackTrace(); 22 } 23 } 24 25 }
1 public static void main(String[] args) 2 { 3 /** 4 * 1.創建線程池 5 * 核心線程池大小:2、 最大線程池大小:4、 工作隊列:有界阻塞隊列,隊列大小為6 線程活動保持時間:10、 活動時間單位:毫秒、 線程工廠:默認、 飽和策略:默認 6 */ 7 ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(6)); 8 threadPoolTest(executor); 9 } 10 11 public static void threadPoolTest(ThreadPoolExecutor executor){ 12 /** 13 * 前提:核心線程數為2,隊列為6,而最大線程池為4 14 * 測試:向線程池中提交10個任務 15 * 猜想:核心線程池創建2個線程執行2個任務,6個任務放到隊列中然後繼續等待執行,2個任務被線程池創建的其他兩個線程執行 16 * */ 17 for (int i = 0; i < 10; i++) 18 { 19 executor.execute(new ThreadPoolTaskTest(i)); 20 } 21 }
執行結果如下:
1 線程:pool-1-thread-3執行任務:8 2 線程:pool-1-thread-1執行任務:0 3 線程:pool-1-thread-2執行任務:1 4 線程:pool-1-thread-4執行任務:9 5 線程:pool-1-thread-1執行任務:4 6 線程:pool-1-thread-4執行任務:5 7 線程:pool-1-thread-3執行任務:2 8 線程:pool-1-thread-2執行任務:3 9 線程:pool-1-thread-1執行任務:6 10 線程:pool-1-thread-4執行任務:7
當將任務數量增加到12個,則核心線程池2個,工作隊列6個,再創建兩個線程執行2個,還有2個任務將會被飽和策略直接拒絕,結果如下
1 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task [email protected]2 rejected from [email protected][Running, pool size = 4, active threads = 4, queued tasks = 6, completed tasks = 0] 2 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047) 3 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823) 4 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369) 5 at com.lucky.test.jvmtest.ThreadPoolTest.threadPoolTest1(ThreadPoolTest.java:32) 6 at com.lucky.test.jvmtest.ThreadPoolTest.main(ThreadPoolTest.java:21) 7 線程:pool-1-thread-4執行任務:9 8 線程:pool-1-thread-1執行任務:0 9 線程:pool-1-thread-3執行任務:8 10 線程:pool-1-thread-2執行任務:1 11 線程:pool-1-thread-2執行任務:5 12 線程:pool-1-thread-1執行任務:3 13 線程:pool-1-thread-3執行任務:4 14 線程:pool-1-thread-4執行任務:2 15 線程:pool-1-thread-1執行任務:7 16 線程:pool-1-thread-2執行任務:6
修改飽和策略,采用CallerRunsPolicy飽和策略,則其他十個任務正常執行,另外兩個任務將會由提交任務的線程來執行任務,結果如下:
1 線程:main執行任務:10 2 線程:main執行任務:11 3 線程:pool-1-thread-2執行任務:1 4 線程:pool-1-thread-3執行任務:8 5 線程:pool-1-thread-4執行任務:9 6 線程:pool-1-thread-1執行任務:0 7 線程:pool-1-thread-1執行任務:3 8 線程:pool-1-thread-4執行任務:4 9 線程:pool-1-thread-3執行任務:2 10 線程:pool-1-thread-2執行任務:5 11 線程:pool-1-thread-4執行任務:7 12 線程:pool-1-thread-1執行任務:6
總結:本文主要整理了線程池的基本知識點及大致用法,下篇將針對Executor框架的多種線程池分別整理
深入淺出JAVA線程池使用原理1