你們好,我是 why,一個四川好男人。html
今天原本應該是武漢馬拉松鳴槍起跑的日子,因此先荒腔走板說幾句馬拉松吧。java
上面的圖是我跑 2019 年成都馬拉松的時候拍的,是一對雙胞胎陪着 80 歲的父親跑全程馬拉松。面試
圖片中的老人叫羅廣德,在他 75 歲以前的人生和其餘的老人並沒有不一樣。編程
可是通過他兒子的影響,在 75 歲的時候開始接觸跑步的。一直就沒有停下腳步,世界六大馬拉松賽(紐約、倫敦、柏林、芝加哥、東京、波士頓)他已經完成了五個。服務器
原本打算今年 4 月份站上波士頓馬拉松的賽道上,完成最後的挑戰。微信
完成以後,他就是世界華人這個年齡段裏第一個完成世界六大馬拉松賽的大滿貫跑者。併發
可是因爲疫情的緣由,波士頓馬拉松延期舉行了。可是沒有關係,我相信老爺子的執着,我也相信他會是第一人。less
他說:「人生沒有太晚的開始,關鍵是要行動起來。如今的年輕朋友不少都缺少鍛鍊,做息時間很差,我但願年輕人都行動起來,我 80 歲都能跑步,難道大家不能跑嗎?」運維
我以前說過,在賽道上你能看到不少有趣的、感動的畫面。我喜歡跑馬拉松,由於跑完以後老是能帶給我爆棚的正能量。分佈式
人生須要一場馬拉松,你能夠遲到,可是你不能缺席。
好了,說迴文章。
經典面試題
此次的文章仍是繞回了我寫的第三篇原創文章《有的線程它死了,因而它變成一道面試題》中留下的幾個問題:
哎,兜兜轉轉,走走停停。天道好輪迴,蒼天饒過誰?
在這篇文章中我主要回答上面拋出的這個問題:你這幾個參數的值怎麼來的呀?
要回答這個問題,咱們得先說說這幾個參數是什麼,請看截圖:
其實,官方的註釋寫的都很是明白了。你看文章的時必定要結合英文,由於英文是 Doug Lea(做者)他本身寫的,表達的是做者本身的準確的想法。
不要瞎猜好嗎?
1.corePoolSize:the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
(核心線程數大小:無論它們建立之後是否是空閒的。線程池須要保持 corePoolSize 數量的線程,除非設置了 allowCoreThreadTimeOut。)
2.maximumPoolSize:the maximum number of threads to allow in the pool。
(最大線程數:線程池中最多容許建立 maximumPoolSize 個線程。)
3.keepAliveTime:when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating。
(存活時間:若是通過 keepAliveTime 時間後,超過核心線程數的線程尚未接受到新的任務,那就回收。)
4.unit:the time unit for the {@code keepAliveTime} argument
(keepAliveTime 的時間單位。)
5.workQueue:the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method。
(存放待執行任務的隊列:當提交的任務數超過核心線程數大小後,再提交的任務就存放在這裏。它僅僅用來存放被 execute 方法提交的 Runnable 任務。因此這裏就不要翻譯爲工做隊列了,好嗎?不要本身給本身挖坑。)
6.threadFactory:the factory to use when the executor creates a new thread。
(線程工程:用來建立線程工廠。好比這裏面能夠自定義線程名稱,當進行虛擬機棧分析時,看着名字就知道這個線程是哪裏來的,不會懵逼。)
7.handler:the handler to use when execution is blocked because the thread bounds and queue capacities are reached。
(拒絕策略:當隊列裏面放滿了任務、最大線程數的線程都在工做時,這時繼續提交的任務線程池就處理不了,應該執行怎麼樣的拒絕策略。)
7 個參數介紹完了,我但願當面試官問你自定義線程池能夠指定哪些參數的時候,你能回答的上來。
固然,不能死記硬背,這樣回答起來磕磕絆絆的,像是在背書。也最好別給我回答什麼:我給你舉個例子吧,就是一開始有多少多少工人....
不必,真的,直接回答每一個參數的名稱和含義就好了,牛逼的話你就給我說英文也行,我也能聽懂。
這玩意你們都懂,又不抽象,你舉那例子幹啥?拖延時間嗎?
面試要求的是儘可能精簡、準確的回答問題,不要讓面試官去你冗長的回答中提煉關鍵字。
一是面試官面試體驗很差。面試完了後,經常是面試者在強調本身的面試體驗。朋友,你多慮了,你面試體驗很差,回去一頓吐槽,叫你進入下一輪面試的時候,大部分人還不是腆着個臉就來了。面試官的體驗很差,那你是真的沒有下一輪了。
二是面試官面試都是有必定的時間限制的,有限的面試時間內,前面太囉嗦了,能問你的問題就少了。問的問題少了,面試官寫評分表的時候一想,我靠,還有好多問題沒問呢,也不知道這小子能不能回答上來,算了,就不進入下一輪了吧。
好了好了,一不下心又暴露了幾個面試小技巧,扯遠了,說回來。
上面的 7 個參數中,咱們主要須要關心的參數是:corePoolSize、maximumPoolSize、workQueue(隊列長度)。
因此,文本主要討論這個問題:
當咱們自定義線程池的時候 corePoolSize、maximumPoolSize、workQueue(隊列長度)該如何設置?
你覺得我要給你講分 IO 密集型任務或者分 CPU 密集型任務?
不會的,說好的是讓面試官眼前一亮、虎軀一震、直呼牛皮的答案。不騙你。
怎麼虎軀一震的呢?
由於我看到了美團技術團隊發表的一篇文章:《Java線程池實現原理及其在美團業務中的實踐》
第一次看到這篇文章的時候我真是眼前一亮,看到美團的這騷操做,我真是直呼牛皮。
(哎,仍是本身見的太少了。)
這篇文章寫的很好,很全面,好比我以前說的線程執行流程,它配了一張圖,一圖勝千言:
阻塞隊列成員表,盡收眼底:
前面都是些基礎知識,文中的後半部分才拋出了一個實際問題:
線程池使用面臨的核心的問題在於:線程池的參數並很差配置。 一方面線程池的運行機制不是很好理解,配置合理須要強依賴開發人員的我的經驗和知識; 另外一方面,線程池執行的狀況和任務類型相關性較大,IO密集型和CPU密集型的任務運行起來的狀況差別很是大。 這致使業界並無一些成熟的經驗策略幫助開發人員參考。
美團給出的對應的解決方案是什麼呢?
線程池參數動態化。
儘管通過謹慎的評估,仍然不可以保證一次計算出來合適的參數,那麼咱們是否能夠將修改線程池參數的成本降下來,這樣至少能夠發生故障的時候能夠快速調整從而縮短故障恢復的時間呢? 基於這個思考,咱們是否能夠將線程池的參數從代碼中遷移到分佈式配置中心上,實現線程池參數可動態配置和即時生效,線程池參數動態化先後的參數修改流程對好比下:
說實話看到這個圖的時候我想起以前也有這樣的想法的。
由於有一次我這邊有個項目裏面的定時任務用到了線程池,可是核心線程數和隊列長度都設置的比較大,某一次任務觸發後查出了大批數據,經過線程池提交任務,每一個任務裏面都會調用下游服務,致使下游服務長時間的壓力過大,也沒有作限流,因此影響了其對外提供的其餘功能。
因而我叫運維幫我在 Apollo(配置中心)調小了核心線程數,而且重啓了服務。
那一次我就在想,咱們使用的是 Apollo 自然支持動態更新,那我能不能動態的修改線程池呢?
由於那個時候不知道一個構建好了的線程池,它的核心線程數和最大線程數是能夠動態修改的。
因此最開始的想法是監聽到參數變化後,直接弄一個新的線程池把原來的給替換掉。
但這樣的問題是,偷天換日以後,原來的線程池裏面的任務我怎麼處理呢?
我不能等原來的線程池裏面的任務執行完成後再換,由於這個時候任務必定是源源不斷的過來的。
因而就卡在了這個地方。
說來慚愧,這塊源碼我看過幾回,但仍是差點火候,學藝不精,怨不得別人。
爲了避免浪費你的時間,先檢測一下你是否有閱讀本文的基礎知識儲備:
首先,咱們先自定義一個線程池:
拿着這個線程池,當這個線程池在正常工做的前提下,我先問你兩個問題:
1.若是這個線程池接受到了 30 個比較耗時的任務,這個時候線程池的狀態(或者說數據)是怎樣的?
2.在前面 30 個比較耗時的任務還沒執行完成的狀況下,再來多少個任務會觸發拒絕策略?
其實這就是在問你線程池的執行流程了,簡單的說一下就是:
1.當接收到了 30 個比較耗時的任務時,10 個核心線程數都在工做,剩下的 20 個去隊列裏面排隊。這個時候和最大線程數是沒有關係的,因此和線程存活時間也就沒有關係。
2.其實你知道這個線程池最多能接受多少任務,你就知道這個題的答案是什麼了,上面的線程池中最多接受 1000(隊列長度) + 30(最大線程數) = 1030 個任務。因此當已經接收了30個任務的狀況下,若是再來 1000 個比較耗時的任務,這個時候隊列也滿了,最大線程數的線程也都在工做,這個時候線程池滿載了。所以,在前面 30 個比較耗時的任務還沒執行完成的狀況下,再來 1001 個任務,第 1001 個任務就會觸發線程池的拒絕策略了。
這兩個問題你得會,若是答不上來你也別往下看了,大機率看的一臉懵逼。
我建議你先給本文點個贊,接着去網上搜一下線程池執行流程的文章(其實美團的那篇文章也寫了執行流程),寫個 Demo 跑一下,摸清楚了,再來看這篇文章。
對於線程池參數到底如何設置的問題美團的那篇文章提供了一個很好的思路和解決方案,展示的是一個大而全的東西。
可是,對於實施起來的細節就沒有具體的展現了。
因此文本斗膽,站在巨人的肩膀上對細節處進行一些補充說明。
1.現有的解決方案的痛點。
2.動態更新的工做原理是什麼?
3.動態設置的注意點有哪些?
4.如何動態指定隊列長度?
5.這個過程當中涉及到的面試題有哪些?
下面從這五點進行展開說明。
如今市面上大多數的答案都是先區分線程池中的任務是 IO 密集型仍是 CPU 密集型。
若是是 CPU 密集型的,能夠把核心線程數設置爲核心數+1。
爲何要加一呢?
《Java併發編程實戰》一書中給出的緣由是:即便當計算(CPU)密集型的線程偶爾因爲頁缺失故障或者其餘緣由而暫停時,這個「額外」的線程也能確保 CPU 的時鐘週期不會被浪費。
看不懂是否是?不要緊我也看不懂。反正把它理解爲一個備份的線程就好了。
這個地方還有個須要注意的小點就是,若是你的服務器上部署的不止一個應用,你就得考慮其餘的應用的線程池配置狀況。
通過精密的計算,你咔一下設置爲核心數,結果項目部署上去了,發現還有其餘的應用在和你搶 CPU,你想一想難不難受。
若是是包含 IO 操做的任務呢?這個纔是咱們關心的東西。
《Java併發編程實戰》一書中給出的計算方式是這樣的:
理想很豐滿,現實很骨感。
我以前有個系統就是按照這個公式算出來的參數去配置的。
結果效果並很差,甚至讓下游系統直呼受不了。
這個東西怎麼說呢,仍是得記住,面試的時候有用。真實場景中只能獲得一個參考值,基於這個參考值,再去進行調整。
咱們再看一下美團的那篇文章調研的現有解決方案列表:
第一個就是咱們上面說的,和實際業務場景有所偏離。
第二個設置爲 2*CPU 核心數,有點像是把任務都當作 IO 密集型去處理了。並且一個項目裏面通常來講不止一個自定義線程池吧?好比有專門處理數據上送的線程池,有專門處理查詢請求的線程池,這樣去作一個簡單的線程隔離。可是若是都用這樣的參數配置的話,顯然是不合理的。
第三個不說了,理想狀態。流量是不可能這麼均衡的,就拿美團來講,下午3,4點的流量,能和 12 點左右午餐時的流量比嗎?
基於上面的這些解決方案的痛點,美團給出了動態化配置的解決方案。
先來一個動態更新的代碼示例:
上面的程序就是自定義了一個核心線程數爲 2,最大線程數爲 5,隊列長度爲 10 的線程池。
而後給它塞 15 個耗時 10 秒的任務,直接讓它 5 個最大線程都在工做,隊列長度 10 個都塞滿。
當前的狀況下,隊列裏面的 10 個,前 5 個在 10 秒後會被執行,後 5 個在 20 秒後會被執行。
再加上最大線程數正在執行的 5 個,15 個任務所有執行徹底須要 3 個 10 秒即 30 秒的時間。
這個時候,若是咱們把核心線程數和最大線程數都修改成 10。
那麼 10 個任務會直接被 10 個最大線程數接管,10 秒就會被處理完成。
剩下的 5 個任務會在 10 秒後被執行完成。
因此,15 個任務執行完成須要 2 個 10 秒即 20 秒的時間處理完成了。
看一下上面程序的打印日誌:
效果實現了,我先看一下原理是什麼。
先看 setCorePoolSize 方法:
這個方法在美團的文章中也說明了:
在運行期線程池使用方調用此方法設置corePoolSize以後,線程池會直接覆蓋原來的corePoolSize值,而且基於當前值和原始值的比較結果採起不一樣的處理策略。
對於當前值小於當前工做線程數的狀況,說明有多餘的worker線程,此時會向當前idle的worker線程發起中斷請求以實現回收,多餘的worker在下次idel的時候也會被回收;
對於當前值大於原始值且當前隊列中有待執行任務,則線程池會建立新的worker線程來執行隊列任務,setCorePoolSize具體流程以下:
看了美團的那篇文章後,我又去看了 Spring 的 ThreadPoolTaskExecutor類 (就是對JDK ThreadPoolExecutor 的一層包裝,能夠理解爲裝飾者模式)的 setCorePoolSize 方法: 註釋上寫的清清楚楚,能夠在線程池運行時修改該參數。
並且,你再品一品 JDK 的源碼,其實源碼也體現出了有修改的含義的,兩個值去作差值,只是第一次設置的時候原來的值爲 0 而已。
哎,當時沒有細細研究,恨本身看源碼的時候不仔細。
接着看 setMaximumPoolSize 源碼:
這個地方就很簡單了,邏輯不太複雜。
1.首先是參數合法性校驗。
2.而後用傳遞進來的值,覆蓋原來的值。
3.判斷工做線程是不是大於最大線程數,若是大於,則對空閒線程發起中斷請求。
通過前面兩個方法的分析,咱們知道了最大線程數和核心線程數能夠動態調整。
調整的時候可能會出現核心線程數調整以後無效的狀況,好比下面這種:
改變以前的核心線程數是 2,最大線程數爲 5,咱們動態修改核心線程數爲 10。
可是從日誌仍是能夠看出,修改以後核心線程數確實變成了 10,但活躍線程數仍是爲 5。
並且我調用了 prestartCoreThread 方法,該方法見名知意,你也知道是啓動全部的核心線程數,全部不存在線程沒有建立的問題。
這是爲何呢?
源碼之下無祕密,我帶你去看一眼:
java.util.concurrent.ThreadPoolExecutor#getTask
在這個方法中咱們能夠看到,若是工做線程數大於最大線程數,則對工做線程數量進行減一操做,而後返回 null。
因此,這個地方的實際流程應該是: 建立新的工做線程 worker,而後工做線程數進行加一操做。 運行建立的工做線程 worker,開始獲取任務 task。 工做線程數量大於最大線程數,對工做線程數進行減一操做。 返回 null,即沒有獲取到 task。 清理該任務,流程結束。
這樣一加一減,因此真正在執行任務的工做線程數的數量一直沒有發生變化,也就是最大線程數。
怎麼解決這個問題呢?
答案已經呼之欲出啦。
設置核心線程數的時候,同時設置最大線程數便可。其實能夠把兩者設置爲相同的值:
這樣,活動線程數就能正常提升了。
有的小夥伴就會問了:若是調整以後把活動線程數設置的值太大了,豈不是業務低峯期咱們還須要人工把值調的小一點?
不存在的,還記得前面介紹 corePoolSize 參數的含義時的註解嗎:
當 allowCoreThreadTimeOut 參數設置爲 true 的時候,核心線程在空閒了 keepAliveTime 的時間後也會被回收的,至關於線程池自動給你動態修改了。
前面介紹了最大線程數和核心線程數的動態設置,可是你發現了嗎,並無設置隊列長度的 set 方法啊?
有的小機靈鬼說先獲取 Queue 對象出來再看一下呢?
仍是沒有,這可咋整呢?
首先咱們看一下爲何沒有提供隊列長度的 set 方法呢:
由於隊列的 capacity 是被 final 修飾了呀。
可是美團的那篇文章明明說了,他們也支持隊列的動態調整呀:
但是沒有詳細說明,可是彆着急,接着看後面的內容能夠發現他們有一個名字爲 ResizableCapacityLinkedBlockIngQueue 的隊列:
很明顯,這是一個自定義隊列了。
咱們也能夠按照這個思路自定義一個隊列,讓其能夠對 Capacity 參數進行修改便可。
操做起來也很是方便,把 LinkedBlockingQueue 粘貼一份出來,修改個名字,而後把 Capacity 參數的 final 修飾符去掉,並提供其對應的 get/set 方法。
而後在程序裏面把原來的隊列換掉:
運行起來看看效果:
能夠看到,隊列大小確實從 10 變成了 100,隊列使用度從 100% 降到了 9%。
我後來去看了美團的那篇文章下面的評論,有個評論是這樣的:
果真不出我所料。
問題一:線程池被建立后里面有線程嗎?若是沒有的話,你知道有什麼方法對線程池進行預熱嗎?
線程池被建立後若是沒有任務過來,裏面是不會有線程的。若是須要預熱的話能夠調用下面的兩個方法:
所有啓動:
僅啓動一個:
問題二:核心線程數會被回收嗎?須要什麼設置?
核心線程數默認是不會被回收的,若是須要回收核心線程數,須要調用下面的方法:
allowCoreThreadTimeOut 該值默認爲 false。
點個贊吧,周更很累的,不要白嫖我,須要一點正反饋。
才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,因爲本號沒有留言功能,還請你加我微信給我指出來,我對其加以修改。(我每篇技術文章都有這句話,我是認真的說的。)
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是why技術,一個不是大佬,可是喜歡分享,又暖又有料的四川好男人。
歡迎關注公衆號【why技術】,堅持輸出原創。分享技術、品味生活,願你我共同進步。