更新時間:2019-09-24 09:39:35 來源:動力節點 瀏覽2135次
線程池是多線程編程中的核心概念,簡單來說就是一組可以執行任務的空閑線程。
首先,我們了解一下多線程框架模型,明白為什么需要線程池。
線程是在一個進程中可以執行一系列指令的執行環境,或稱運行程序。多線程編程指的是用多個線程并行執行多個任務。當然,JVM對多線程有良好的支持。
盡管這帶來了諸多優勢,首當其沖的就是程序性能提高,但多線程編程也有缺點——增加了代碼復雜度、同步問題、非預期結果和增加創建線程的開銷。
在這篇文章中,我們來了解一下如何使用Java線程池來緩解這些問題。
為什么使用線程池?
創建并開啟一個線程開銷很大。如果我們每次需要執行任務時重復這個步驟,那將會是一筆巨大的性能開銷,這也是我們希望通過多線程解決的問題。
為了更好理解創建和開啟一個線程的開銷,讓我們來看一看JVM在后臺做了哪些事:
為線程棧分配內存,保存每個線程方法調用的棧幀。
每個棧幀包括本地變量數組、返回值、操作棧和常量池
一些JVM支持本地方法,也將分配本地方法棧
每個線程獲得一個程序計數器,標識處理器正在執行哪條指令
系統創建本地線程,與Java線程對應
和線程相關的描述符被添加到JVM內部數據結構
線程共享堆和方法區
當然,這些步驟的具體細節取決于JVM和操作系統。
另外,更多的線程意味著更多工作量,系統需要調度和決定哪個線程接下來可以訪問資源。
線程池通過減少需要的線程數量并管理線程生命周期,來幫助我們緩解性能問題。
本質上,線程在我們使用前一直保存在線程池中,在執行完任務之后,線程會返回線程池等待下次使用。這種機制在執行很多小任務的系統中十分有用。
Java線程池
Java通過executor對象來實現自己的線程池模型。可以使用executor接口或其他線程池的實現,它們都允許細粒度的控制。
java.util.concurrent包中有以下接口:
Executor——執行任務的簡單接口
ExecutorService——一個較復雜的接口,包含額外方法來管理任務和executor本身
ScheduledExecutorService——擴展自ExecutorService,增加了執行任務的調度方法
除了這些接口,這個包中也提供了Executors類直接獲取實現了這些接口的executor實例
一般來說,一個Java線程池包含以下部分:
工作線程的池子,負責管理線程
線程工廠,負責創建新線程
等待執行的任務隊列
在下面的章節,讓我們仔細看一看Java類和接口如何為線程池提供支持。
Executors類和Executor接口
Executors類包含工廠方法創建不同類型的線程池,Executor是個簡單的線程池接口,只有一個execute()方法。
我們通過一個例子來結合使用這兩個類(接口),首先創建一個單線程的線程池,然后用它執行一個簡單的語句:
Executorexecutor=Executors.newSingleThreadExecutor();
executor.execute(()->System.out.println("Singlethreadpooltest"));
注意語句寫成了lambda表達式,會被自動推斷成Runnable類型。
如果有工作線程可用,execute()方法將執行語句,否則就把Runnable任務放進隊列,等待線程可用。
基本上,executor代替了顯式創建和管理線程。
Executors類里的工廠方法可以創建很多類型的線程池:
newSingleThreadExecutor():包含單個線程和無界隊列的線程池,同一時間只能執行一個任務
newFixedThreadPool():包含固定數量線程并共享無界隊列的線程池;當所有線程處于工作狀態,有新任務提交時,任務在隊列中等待,直到一個線程變為可用狀態
newCachedThreadPool():只有需要時創建新線程的線程池
newWorkStealingThreadPool():基于工作竊取(work-stealing)算法的線程池,后面章節詳細說明
接下來,讓我們看一下ExecutorService接口提供了哪些新功能
ExecutorService
創建ExecutorService方式之一便是通過Excutors類的工廠方法。
ExecutorServiceexecutor=Executors.newFixedThreadPool(10);
Besidestheexecute()method,thisinterfacealsodefinesasimilarsubmit()methodthatcanreturnaFutureobject:
除了execute()方法,接口也定義了相似的submit()方法,這個方法可以返回一個Future對象。
Callable<Double>callableTask=()->{
returnemployeeService.calculateBonus(employee);
};
Future<Double>future=executor.submit(callableTask);
//executeotheroperations
try{
if(future.isDone()){
doubleresult=future.get();
}
}catch(InterruptedException|ExecutionExceptione){
e.printStackTrace();
}
從上面的例子可以看到,Future接口可以返回Callable類型任務的結果,而且能顯示任務的執行狀態。
當沒有任務等待執行時,ExecutorService并不會自動銷毀,所以你可以使用shutdown()或shutdownNow()來顯式關閉它。
executor.shutdown();
ScheduledExecutorService
這是ExecutorService的一個子接口,增加了調度任務的方法。
ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(10);
schedule()方法的參數指定執行的方法、延時和TimeUnit
Future<Double>future=executor.schedule(callableTask,2,TimeUnit.MILLISECONDS);
另外,這個接口定義了其他兩個方法:
executor.scheduleAtFixedRate(
()->System.out.println("FixedRateScheduled"),2,2000,TimeUnit.MILLISECONDS);
executor.scheduleWithFixedDelay(
()->System.out.println("FixedDelayScheduled"),2,2000,TimeUnit.MILLISECONDS);
scheduleAtFixedRate()方法延時2毫秒執行任務,然后每2秒重復一次。相似的,scheduleWithFixedDelay()方法延時2毫秒后執行第一次,然后在上一次執行完成2秒后再次重復執行。
在下面的章節,我們來看一下ExecutorService接口的兩個實現:ThreadPoolExecutor和ForkJoinPool。
ThreadPoolExecutor
這個線程池的實現增加了配置參數的能力。創建ThreadPoolExecutor對象最方便的方式就是通過Executors工廠方法:
ThreadPoolExecutorexecutor=(ThreadPoolExecutor)Executors.newFixedThreadPool(10);
這種情況下,線程池按照默認值預配置了參數。線程數量由以下參數控制:
corePoolSize和maximumPoolSize:表示線程數量的范圍
keepAliveTime:決定了額外線程存活時間
我們深入了解一下這些參數如何使用。
當一個任務被提交時,如果執行中的線程數量小于corePoolSize,一個新的線程被創建。如果運行的線程數量大于corePoolSize,但小于maximumPoolSize,并且任務隊列已滿時,依然會創建新的線程。如果多于corePoolSize的線程空閑時間超過keepAliveTime,它們會被終止。
上面那個例子中,newFixedThreadPool()方法創建的線程池,corePoolSize=maximumPoolSize=10并且keepAliveTime為0秒。
如果你使用newCachedThreadPool()方法,創建的線程池maximumPoolSize為Integer.MAX_VALUE,并且keepAliveTime為60秒。
ThreadPoolExecutorcachedPoolExecutor
=(ThreadPoolExecutor)Executors.newCachedThreadPool();
Theparameterscanalsobesetthroughaconstructororthroughsettermethods:
這些參數也可以通過構造函數或setter方法設置:
ThreadPoolExecutorexecutor=newThreadPoolExecutor(
4,6,60,TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>()
);
executor.setMaximumPoolSize(8);
ThreadPoolExecutor的一個子類便是ScheduledThreadPoolExecutor,它實現了ScheduledExecutorService接口。你可以通過newScheduledThreadPool()工廠方法來創建這種類型的線程池。
ScheduledThreadPoolExecutorexecutor
=(ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(5);
上面語句創建了一個線程池,corePoolSize為5,maximumPoolSize無限制,keepAliveTime為0秒。
ForkJoinPool
另一個線程池的實現是ForkJoinPool類。它實現了ExecutorService接口,并且是Java7中fork/join框架的重要組件。
fork/join框架基于“工作竊取算法”。簡而言之,意思就是執行完任務的線程可以從其他運行中的線程“竊取”工作。
ForkJoinPool適用于任務創建子任務的情況,或者外部客戶端創建大量小任務到線程池。
這種線程池的工作流程如下:
創建ForkJoinTask子類
根據某種條件將任務切分成子任務
調用執行任務
將任務結果合并
實例化對象并添加到池中
創建一個ForkJoinTask,你可以選擇RecursiveAction或RecursiveTask這兩個子類,后者有返回值。
我們來實現一個繼承RecursiveTask的類,計算階乘,并把任務根據閾值劃分成子任務。
這個類需要實現的主要方法就是重寫compute()方法,用于合并每個子任務的結果。
具體劃分任務邏輯在createSubtasks()方法中:
最后,calculate()方法包含一定范圍內的乘數。
接下來,任務可以添加到線程池:
ForkJoinPoolpool=ForkJoinPool.commonPool();
BigIntegerresult=pool.invoke(newFactorialTask(100));
ThreadPoolExecutor與ForkJoinPool對比
初看上去,似乎fork/join框架帶來性能提升。但是這取決于你所解決問題的類型。
當選擇線程池時,非常重要的一點是牢記創建、管理線程以及線程間切換執行會帶來的開銷。
ThreadPoolExecutor可以控制線程數量和每個線程執行的任務。這很適合你需要在不同的線程上執行少量巨大的任務。
相比較而言,ForkJoinPool基于線程從其他線程“竊取”任務。正因如此,當任務可以分割成小任務時可以提高效率。
為了實現工作竊取算法,fork/join框架使用兩種隊列:
包含所有任務的主要隊列
每個線程的任務隊列
當線程執行完自己任務隊列中的任務,它們試圖從其他隊列獲取任務。為了使這一過程更加高效,線程任務隊列使用雙端隊列(doubleendedqueue)數據結構,一端與線程交互,另一端用于“竊取”任務。
來自TheHDeveloper的圖很好的表現出了這一過程:
和這種模型相比,ThreadPoolExecutor只使用一個主要隊列。
最后要注意的一點ForkJoinPool只適用于任務可以創建子任務。否則它和ThreadPoolExecutor沒區別,甚至開銷更大。
跟蹤線程池的執行
現在我們對Java線程池生態系統有了基本的了解,讓我們通過一個使用了線程池的應用,來看一看執行中到底發生了什么。
通過在FactorialTask的構造函數和calculate()方法中加入日志語句,你可以看到下面調用序列:
你可以看到創建了很多任務,但只有3個工作線程——所以任務通過線程池被可用線程處理。
也可以看到在放到執行池之前,主線程中對象如何被創建。
使用Prefix這一類可視化的日志工具是一個很棒的方式來探索和理解運行時的線程池。
記錄線程池日志的核心便是保證在日志信息中方便辨識線程名字。Log4J2通過使用布局能夠很好完成這種工作。
使用線程池的潛在風險
盡管線程池有巨大優勢,你在使用中仍會遇到一些問題,比如:
用的線程池過大或過小:如果線程池包含太多線程,會明顯的影響應用的性能;另一方面,線程池太小并不能帶來所期待的性能提升。
正如其他多線程情形一樣,死鎖也會發生。舉個例子,一個任務可能等待另一個任務完成,而后者并沒有可用線程處理執行。所以說避免任務之間的依賴是個好習慣。
等待執行時間很長的任務:為了避免長時間阻塞線程,你可以指定最大等待時間,并決定過期任務是拒絕處理還是重新加入隊列。
為了降低風險,你必須根據要處理的任務,來謹慎選擇線程池的類型和參數。對你的系統進行壓力測試也是值得的,它可以幫你獲取真實環境下的系統行為數據。
結論
線程池有很大優勢,簡單來說就是可以將任務的執行從線程的創建和管理中分離。另外,如果使用得當,它們可以極大提高應用的性能。
如果你學會充分利用線程池,Java生態系統好處便是其中有很多成熟穩定的線程池實現。
以上就是動力節點java培訓機構小編介紹的“深入學習Java線程池:Java線程池學習教程”的內容,希望對大家有幫助,更多java最新資訊請繼續關注動力節點java培訓機構官網,每天會有精彩內容分享與你。
0基礎 0學費 15天面授
有基礎 直達就業
業余時間 高薪轉行
工作1~3年,加薪神器
工作3~5年,晉升架構
提交申請后,顧問老師會電話與您溝通安排學習