Java並發編程實踐讀書筆記(5) 線程池的使用
Executor與Task的耦合性
1,除非線程池很非常大,否則一個Task不要依賴同一個線程服務中的另外一個Task,因為這樣容易造成死鎖;
2,線程的執行是並行的,所以在設計Task的時候要考慮到線程安全問題。如果你認為只會在單任務線程的Executor中運行的話,從設計上講這就已經耦合了。
3,長時間的任務有可能會影響到其他任務的執行效率,可以讓其他線程在等待的時候限定一下等待時間。不要無限制地等待下去。
確定線程池的大小
給出如下定義:
要使CPU達到期望的使用率,線程池的大小應設置為:
約等於Ncpu+1。
定制線程池
通過Executor來創建的線程池其實是預先給我們打造好的線程池,例如:
如果不能滿足需求,可以自己量身定做:
public ThreadPoolExecutor(int corePoolSize, //基本大小,沒有執行任務時線程池的大小 int maximumPoolSize, //最大任務數量 long keepAliveTime, //最大空閑時間,超時後會被回收 TimeUnit unit, BlockingQueue<Runnable> workQueue) { //... }
任務隊列:緩存任務
前面有說到,使用線程池的好處是可以限制線程的數量,因此保證服務器不會被超量的線程給拖垮。當並發請求增加,我們不再是盲目地創建線程了。而是簡單地創建一個任務緩存到處理隊列中了。那麽問題來了,當並發請求繼續激增,服務線程無法消化任務,造成大量的任務堆積到隊列中。這也是會消耗服務器資源的。
我們需要讓服務器實在忙不過來的時候,扔掉一些請求。以保證自己不會奔潰。該放手的時候一定要放手,不是每個人都能隨隨便便造一個航天飛船發上天啊。服務器真的應付不過來的時候最重要的是要保護好自己。
創建線程池的時候,可以自己指定任務隊列:
無界隊列:就是創建隊列的時候不設置大小;
有界隊列:通上設置一個大小;
同步交換隊列:SynchronousQueue,嚴格意義上說,它並不是真的隊列,它只是一種數據交換的工具。任務來之後,這個隊列直接把任務交給線程池中的一個線程。如果沒有可用線程,則創建一個線程。如果不能創建了會拒絕這個任務。同步交換隊列的核心就是它不緩存任務,要麽線程池足夠大,要麽就直接拒絕。用於線程池可以無限增長或不會出現任務激增的場景。
可以用PriorityBlockingQueue來實現任務的排序。
飽和策略:任務隊列中都存不了了怎麽辦
線程池提供了多種處理辦法:
Abort:終止,對外拋RejectedExecutionException;
Discard:悄悄拋棄,註意是悄悄地,客戶端根本不會知道自己提交的任務根本就沒運行;
DiscardOldest:拋棄最老的,如果使用的是優先隊列,被拋棄的會是優先級最高的那個(所以DiscardOldest不要和優先隊列搭配使用)。
Caller-Runs:用客戶端線程來直接運行任務代碼。就是哪個線程在往線程池扔任務,那麽就用這個線程來直接跑這個任務。這麽做的好處是,讓客戶端分擔線程池的負擔,更主要的是讓客戶端先忙一會兒別的,從而暫停往線程池提交任務的動作。以期望線程池能最終緩過來。客戶端都來幫著線程池做事了,那它自己的事肯定也就沒人做了。如果客戶端代碼是一個Web請求處理程序,那麽它將不再accpet新的請求,這些新的請求會被堵在TCP的緩存中,如果系統一直沒有緩過勁來,再繼續發展下去,就會把“過載”的信息彌漫到TCP的調用端了。整個這套設計的潛臺詞是“我們已經盡力了”。系統不會硬著陸直接掛掉,如果請求壓力放緩,系統還是可以扛過來的。
線程工廠:定制線程
可以自己實現線程工廠來構建線程變量,從而定義更多需要的信息。
擴展ThreadPoolExecutor
ThreadPoolExecutor有一些生命周期的方法:beforeExecute、afterExecute和terminated。可以重寫這些方法,實現統計和監控的功能。
遞歸算法的並行化
什麽樣的情況可以並行?
比如for循環去做一些事情,而這些事情是相互獨立的。那麽這些事情其實可以並行來做的(for循環是串行執行)。
這給我們的編程提供了新的思路,很多寫了很多遍、平淡無奇的代碼其實有更有趣的實現方式。
Java並發編程實踐讀書筆記(5) 線程池的使用