小宇:閃客,我最近看到線程池,被裏邊亂七八槽的參數給搞暈了,你能不能給我講講呀?
面試
閃客:沒問題,這個我擅長,我們從一個最簡單的狀況開始,假設有一段代碼,你但願異步執行它,是否是要寫出這樣的代碼?編程
new Thread(r).start();
小宇:嗯嗯,最簡單的寫法彷佛就是這樣呢。 安全
閃客:這種寫法固然能夠完成功能,但是你這樣寫,老王這樣寫,老張也這樣寫,程序中處處都是這樣建立線程的方法,能不能寫一個統一的工具類讓你們調用呢?併發
小宇:能夠的,感受有一個統一的工具類,更優雅一些。異步
閃客:那若是讓你來設計這個工具類,你會怎麼寫呢?我先給你定一個接口,你來實現。工具
public interface Executor { public void execute(Runnable r); }
小宇:emmm,我可能先定義幾個成員變量,好比核心線程數、最大線程數 ...反正就是那些亂七八糟的參數。優化
閃客:STOP!小宇呀,你如今深受面試手冊的毒害,你先把這些所有的概念忘掉,就說讓你寫一個最簡單的工具類,第一反應,你會怎麼寫?this
小宇:那我可能會這樣spa
// 新線程:直接建立一個新線程運行 class FlashExecutor implements Executor { public void execute(Runnable r) { new Thread(r).start(); } }
閃客:嗯嗯很好,你的思路很是棒。 線程
小宇:啊,我這個會不會太 low 了呀,我還覺得你會罵我呢。
怎麼會, Doug Lea 大神在 JDK 源碼註釋中給出的就是這樣的例子,這是最根本的功能。你在這個基礎上,嘗試着優化一下?
小宇:還能怎麼優化呢?這不已經用一個工具類實現了異步執行了嘛!
閃客:我問你一個問題,假若有 10000 我的都調用這個工具類提交任務,那就會建立 10000 個線程來執行,這確定不合適吧!能不能控制一下線程的數量呢?
小宇:這不難,我能夠把這個任務 r 丟到一個 tasks 隊列中,而後只啓動一個線程,就叫它 Worker 線程吧,不斷從 tasks 隊列中取任務,執行任務。這樣不管調用者調用多少次,永遠就只有一個 Worker 線程在運行,像這樣。
閃客:太棒了,這個設計有了三個重大的意義:
1. 控制了線程數量。
2. 隊列不但起到了緩衝的做用,還將任務的提交與執行解耦了。
3. 最重要的一點是,解決了每次重複建立和銷燬線程帶來的系統開銷。
小宇:哇真的麼,這麼小的改動有這麼多意義呀。
閃客:那固然,不過只有一個後臺的工做線程 Worker 會不會少了點?還有若是這個 tasks 隊列滿了怎麼辦呢?
小宇:哦,的確,只有一個線程在某些場景下是很吃力的,那我把 Worker 線程的數量增長?
閃客:沒錯,Worker 線程的數量要增長,可是具體數量要讓使用者決定,調用時傳入,就叫核心線程數 corePoolSize 吧。
小宇:好的,那我這樣設計。
1. 初始化線程池時,直接啓動 corePoolSize 個工做線程 Worker 先跑着。
2. 這些 Worker 就是死循環從隊列裏取任務而後執行。
3. execute 方法仍然是直接把任務放到隊列,但隊列滿了以後直接拋棄
閃客:太完美了,獎勵你一塊費列羅吧。
小宇:哈哈謝謝,那我先吃一下子哈。
閃客:好,你邊吃我邊說。如今咱們已經實現了一個至少不那麼醜陋的線程池了,但還有幾處小瑕疵,好比初始化的時候,就建立了一堆 Worker 線程在那空跑着,假如此時並無異步任務提交過來執行,這就有點浪費了。
小宇:哦好像是誒!
閃客:還有,你這隊列一滿,就直接把新任務丟棄了,這樣有些粗暴,能不能讓調用者本身決定該怎麼處理呢?
小宇:哎呀,想不到我這麼溫柔的妹紙竟然寫出了這麼粗暴的代碼。
閃客:額,你先把費列羅嚥下去吧。
小宇:我吃完了,如今腦子有點不夠用了,得先消化消化食物,要不你幫我分析分析吧。
閃客:好的,如今咱們作出以下改進。
1. 按需建立Worker:剛初始化線程池時,再也不馬上建立 corePoolSize 個工做線程,而是等待調用者不斷提交任務的過程當中,逐漸把工做線程 Worker 建立出來,等數量達到 corePoolSize 時就中止,把任務直接丟到隊列裏。那就必然要用一個屬性記錄已經建立出來的工做線程數量,就叫 workCount 吧。
2. 加拒絕策略:實現上就是增長一個入參,類型是一個接口 RejectedExecutionHandler,由調用者決定實現類,以便在任務提交失敗後執行 rejectedExecution 方法。
3. 增長線程工廠:實現上就是增長一個入參,類型是一個接口 ThreadFactory,增長工做線程時再也不直接 new 線程,而是調用這個由調用者傳入的 ThreadFactory 實現類的 newThread 方法。
就像下面這樣。
小宇:哇,仍是你厲害,這一版應該很完美了吧? 閃客:不不不,離完美還差得很遠,接下來的改進,由你來想吧,我這裏能夠給你一個提示
彈性思惟
小宇:彈性思惟?哈哈閃客你這術語說的真是愈來愈不像人話了
閃客:咳咳
小宇:哦,我是說你確定是指我這個代碼寫的沒有彈性,對吧?但是彈性是指什麼呢?
閃客:簡單說,在這個場景裏,彈性就是在任務提交比較頻繁,和任務提交很是不頻繁這兩種狀況下,你這個代碼是否有問題?
小宇:emmm 讓我想一想,我這個線程池,當提交任務的量突增時,工做線程和隊列都被佔滿了,就只能走拒絕策略,其實就是被丟棄掉
閃客:是的
小宇:這樣的確是太硬了,誒不過我想了下,調用方能夠經過設置很大的核心線程數 corePoolSize 來解決這個問題呀。
閃客:的確是能夠,但通常場景下 QPS 高峯期都很短,而爲了這個很短暫的高峯,設置很大的核心線程數,簡直太浪費資源了,你看上面的圖不以爲眼暈麼?
小宇:是呀,那怎麼辦呢,太大了也不行,過小了也不行。
閃客:咱們能夠發明一個新的屬性,叫最大線程數 maximumPoolSize 。當核心線程數和隊列都滿了時,新提交的任務仍然能夠經過建立新的工做線程(叫它 非核心線程 ),直到工做線程數達到 maximumPoolSize 爲止,這樣就能夠緩解一時的高峯期了,而用戶也不用設置過大的核心線程數。
小宇:哦好像有點感受了,但是具體怎麼操做呢? 閃客:想象力不行呀小宇,那你看下面的演示。
1. 開始的時候和上一版同樣,當 workCount < corePoolSize 時,經過建立新的 Worker 來執行任務。
2. 當 workCount >= corePoolSize 就中止建立新線程,把任務直接丟到隊列裏。
3. 但當隊列已滿且仍然 workCount < maximumPoolSize 時,再也不直接走拒絕策略,而是建立非核心線程,直到 workCount = maximumPoolSize,再走拒絕策略。
小宇:哎呀,我怎麼沒想到,這樣 corePoolSize 就負責平時大多數狀況所須要的工做線程數,而 maximumPoolSize 就負責在高峯期臨時擴充工做線程數。
閃客:沒錯,高峯時期的彈性搞定了,那天然就還要考慮低谷時期。當長時間沒有任務提交時,核心線程與非核心線程都一直空跑着,浪費資源。咱們能夠給非核心線程設定一個超時時間 keepAliveTime ,當這麼長時間沒能從隊列裏獲取任務時,就再也不等了,銷燬線程。
小宇:嗯,這回我們的線程池在 QPS 高峯時能夠臨時擴容,QPS 低谷時又能夠及時回收線程(非核心線程)而不至於浪費資源,真的顯得十分 Q 彈呢。
閃客:是呀是呀。誒不對,怎麼又變成我說了,不是說這一版你來思考麼?
小宇:我也想啊,但你這一講技術就自說自話的毛病總是不改,我有啥辦法。 閃客:額抱歉抱歉,那接下來你總結一下咱們的線程池吧
小宇:嗯好的,首先它的構造方法是這個樣子滴
public FlashExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... // 省略一些參數校驗 this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
這些參數分別是
int corePoolSize:核心線程數
int maximumPoolSize:最大線程數
long keepAliveTime:非核心線程的空閒時間
TimeUnit unit:空閒時間的單位
BlockingQueue<Runnable> workQueue:任務隊列(線程安全的阻塞隊列)
ThreadFactory threadFactory:線程工廠
RejectedExecutionHandler handler:拒絕策略
整個任務的提交流程是
閃客:不錯不錯,這但是你本身總結的喲,如今還用我給你講什麼是線程池了麼?
小宇:啊天呢,我才發現這彷佛就是我一直弄不清楚的線程池的參數和原理呢!
閃客:沒錯,並且最後一版代碼的構造方法,就是 Java 面試常考的 ThreadPoolExecutor 最長的那個構造方法,參數名都沒變。
小宇:哇,太讚了!我都忘了一開始我想幹嗎了,嘻嘻。
閃客:哈哈,不知不覺學到了技術才爽呢,對吧?晚飯時間快到了,要不要一塊去吃山西面館呀?
小宇:哦,那家店餐桌的顏色我不太喜歡,下次吧。
閃客:哦好吧。
後記
線程池是面試常考的知識點,網上不少文章都是直接從它那有 7 個參數的構造方法講起,強行把各個參數的含義說給你聽,讓人云裏霧裏。 但願讀者讀完本篇文章後,線程池的這些參數再也不是死記硬背,而是像本文中這些動圖同樣在你的腦中活靈活現,這樣就能永遠記住他們啦~ 本文中各個版本都有對應代碼,在公衆號 低併發編程 後臺回覆 線程池 便可獲取。