2 w字長文帶你深入理解線程池

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程池可以說是 Java 進階必備的知識點了,也是面試中必備的考點,可能不少人看了這篇文章後能對線程池工作原理說上一二,但這還遠遠不夠,如果碰到比較有經驗的面試官再繼續追問,很可能會被吊打,考慮如下問題:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Tomcat 的線程池和 JDK 的線程池實現有啥區別, Dubbo 中有類似 Tomcat 的線程池實現嗎?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"我司網關 dubbo 調用線程池曾經出現過這樣的一個問題:壓測時接口可以正常返回,但接口 RT 很高,假設設置的核心線程大小爲 500,最大線程爲 800,緩衝隊列爲 5000,你能從這個設置中發現出一些問題並對這些參數進行調優嗎?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"線程池裏的線程真的有核心線程和非核心線程之分?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"線程池被 shutdown 後,還能產生新的線程?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"線程把任務丟給線程池後肯定就馬上返回了?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"線程池裏的線程異常後會再次新增線程嗎,如何捕獲這些線程拋出的異常?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"線程池的大小如何設置,如何"},{"type":"text","marks":[{"type":"strong"}],"text":"動態設置"},{"type":"text","text":"線程池的參數"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":8,"align":null,"origin":null},"content":[{"type":"text","text":"線程池的狀態機畫一下?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":9,"align":null,"origin":null},"content":[{"type":"text","text":"阿里 Java 代碼規範爲什麼不允許使用 Executors 快速創建線程池?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":10,"align":null,"origin":null},"content":[{"type":"text","text":"使用線程池應該避免哪些問題,能否簡單說下線程池的最佳實踐?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":11,"align":null,"origin":null},"content":[{"type":"text","text":"如何優雅關閉線程池"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":12,"align":null,"origin":null},"content":[{"type":"text","text":"如何對線程池進行監控"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相信不少人看了這些問題會有些懵"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/65/6510d5471f36616fb53c0bae7d68e266.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 其實這些問題的答案大多數都藏在線程池的源碼裏,所以深入瞭解線程池的源碼非常重要,本章我們將會來學習一下線程池的源碼,相信看完之後,以上的問題大部分都能回答,另外一些問題我們也會在文中與大家一起探討。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文將會從以下幾個方面來介紹線程池的原理。"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼要用線程池"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"線程池是如何工作的"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"線程池提交任務的兩種方式"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"ThreadPoolExecutor 源碼剖析"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"解答開篇的問題"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"線程池的最佳實踐"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"總結"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相信大家看完對線程池的理解會更進一步,肝文不易,看完別完了三連哦。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"爲什麼要用線程池"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上文也提到過,創建線程有三大開銷,如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、其實 Java 中的線程模型是基於操作系統原生線程模型實現的,也就是說 Java 中的線程其實是基於內核線程實現的,線程的創建,析構與同步都需要進行系統調用,而系統調用需要在用戶態與內核中來回切換,代價相對較高,線程的生命週期包括「線程創建時間」,「線程執行任務時間」,「線程銷燬時間」,創建和銷燬都需要導致系統調用。2、每個 Thread 都需要有一個內核線程的支持,也就意味着每個 Thread 都需要消耗一定的內核資源(如內核線程的棧空間),因此能創建的 Thread 是有限的,默認一個線程的線程棧大小是 1 M,有圖有真相"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/66/66fb9e1623b64f703abc0b65d255e325.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖中所示,在 Java 8 下,創建 19 個線程(thread #19)需要創建 19535 KB,即 1 M 左右,reserved 代表如果創建 19 個線程,操作系統保證會爲其分配這麼多空間(實際上並不一定分配),committed 則表示實際已分配的空間大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"畫外音:注意,這是在 Java 8 下的線程佔用空間情況,但在 Java 11 中,對線程作了很大的優化,創建一個線程大概只需要 40 KB,空間消耗大大減少"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、線程多了,導致不可忽視的上下文切換開銷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此可見,線程的創建是昂貴的,所以必須以線程池的形式來管理這些線程,在線程池中合理設置線程大小和管理線程,以達到以"},{"type":"text","marks":[{"type":"strong"}],"text":"合理的創建線程大小以達到最大化收益,最小化風險的目的"},{"type":"text","text":",對於開發人員來說,要完成任務不用關心線程如何創建,如何銷燬,如何協作,只需要關心提交的任務何時完成即可,對線程的調優,監控等這些細枝末節的工作通通交給線程池來實現,所以也讓開發人員得到極大的解脫!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似線程池的這種池化思想應用在很多地方,比如數據庫連接池,Http 連接池等,避免了昂貴資源的創建,提升了性能,也解放了開發人員。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"ThreadPoolExecutor 設計架構圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們來看看 Executor 框架的設計圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/92/92865f26dcae33fe0264ba6e27d3445b.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Executor: 最頂層的 Executor 接口只提供了一個 execute 接口,實現了提交任務與執行任務的解藕,這個方法是最核心的,也是我們源碼剖析的重點,此方法最終是由 ThreadPoolExecutor 實現的,"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ExecutorService 擴展了 Executor 接口,實現了終止執行器,單個/批量提交任務等方法"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AbstractExecutorService 實現了 ExecutorService 接口,實現了除 execute 以外的所有方法,只將一個最重要的 execute 方法交給 ThreadPoolExecutor 實現。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣的分層設計雖然層次看起來挺多,但每一層每司其職,邏輯清晰,值得借鑑。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"線程池是如何工作的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們來看下如何創建一個線程池"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20, 600L,"}]},{"type":"codeinline","content":[{"type":"text","text":"                    TimeUnit.SECONDS, new LinkedBlockingQueue<>(4096),"}]},{"type":"codeinline","content":[{"type":"text","text":"                    new NamedThreadFactory(\"common-work-thread\"));"}]},{"type":"codeinline","content":[{"type":"text","text":"// 設置拒絕策略,默認爲 AbortPolicy"}]},{"type":"codeinline","content":[{"type":"text","text":"threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看下其構造方法簽名如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"public ThreadPoolExecutor(int corePoolSize,"}]},{"type":"codeinline","content":[{"type":"text","text":"                              int maximumPoolSize,"}]},{"type":"codeinline","content":[{"type":"text","text":"                              long keepAliveTime,"}]},{"type":"codeinline","content":[{"type":"text","text":"                              TimeUnit unit,"}]},{"type":"codeinline","content":[{"type":"text","text":"                              BlockingQueue workQueue,"}]},{"type":"codeinline","content":[{"type":"text","text":"                              ThreadFactory threadFactory,"}]},{"type":"codeinline","content":[{"type":"text","text":"                              RejectedExecutionHandler handler) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 省略代碼若干"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要理解這些參數具體代表的意義,必須清楚線程池提交任務與執行任務流程,如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b7/b75631464fe0dfca7be00f1c919fa90a.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 圖片來自美團技術團隊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"步驟如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、corePoolSize:如果提交任務後線程還在運行,當線程數小於 corePoolSize 值時,無論線程池中的線程是否忙碌,都會創建線程,並把任務交給此新創建的線程進行處理,如果線程數少於等於 corePoolSize,那麼這些線程不會回收,除非將 allowCoreThreadTimeOut 設置爲 true,但一般不這麼幹,因爲頻繁地創建銷燬線程會極大地增加系統調用的開銷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、workQueue:如果線程數大於核心數(corePoolSize)且小於最大線程數(maximumPoolSize),則會將任務先丟到阻塞隊列裏,然後線程自己去阻塞隊列中拉取任務執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、maximumPoolSize: 線程池中最大可創建的線程數,如果提交任務時隊列滿了且線程數未到達這個設定值,則會創建線程並執行此次提交的任務,如果提交任務時隊列滿了但線池數已經到達了這個值,此時說明已經超出了線池程的負載能力,就會執行拒絕策略,這也好理解,總不能讓源源不斷地任務進來把線程池給壓垮了吧,我們首先要保證線程池能正常工作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、RejectedExecutionHandler:一共有以下四種拒絕策略"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AbortPolicy:丟棄任務並拋出異常,這也是默認策略;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CallerRunsPolicy:用調用者所在的線程來執行任務,所以開頭的問題「線程把任務丟給線程池後肯定就馬上返回了?」我們可以回答了,如果用的是 CallerRunsPolicy 策略,提交任務的線程(比如主線程)提交任務後並不能保證馬上就返回,當觸發了這個 reject 策略不得不親自來處理這個任務。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,並執行當前任務。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DiscardPolicy:直接丟棄任務,不拋出任何異常,這種策略只適用於不重要的任務。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5、keepAliveTime: 線程存活時間,如果在此時間內超出 corePoolSize 大小的線程處於 idle 狀態,這些線程會被回收"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6、threadFactory:可以用此參數設置線程池的命名,指定 defaultUncaughtExceptionHandler(有啥用,後文闡述),甚至可以設定線程爲守護線程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在問題來了,該如何合理設置這些參數呢。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"首先來看線程大小設置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"<>告訴我們應該分兩種情況"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"針對 CPU 密集型的任務,在有 Ncpu個處理器的系統上,當線程池的大小爲 Ncpu + 1 時,通常能實現最優的利用率,+1 是因爲當計算密集型線程偶爾由於缺頁故障或其他原因而暫停工作時,這個\"額外\"的線程也能確保 CPU 的時鐘週期不會被浪費,所謂 CPU 密集,就是線程一直在忙碌,這樣將線程池的大小設置爲 Ncpu + 1 避免了線程的上下文切換,讓線程時刻處於忙碌狀態,將 CPU 的利用率最大化。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"針對 IO 密集型的任務,它也給出瞭如下計算公式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d5/d568486af35addc54adbaa3b7783efad.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些公式看看就好,實際的業務場景中基本用不上,這些公式太過理論化了,脫離業務場景,僅可作個理論參考,舉個例子,你說 CPU 密集型任務設置線程池大小爲 N + 1個,但實際上在業務中往往不只設置一個線程池,這種情況套用的公式就懵逼了"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/31/313405dcfaacde4fab456135b20ea139.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"再來看 workQueue 的大小設置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上文可知,如果最大線程大於核心線程數,當且僅當核心線程滿了且 workQueue 也滿的情況下,纔會新增新的線程,也就是說如果 workQueue 是無界隊列,那麼當線程數增加到 corePoolSize 後,永遠不會再新增新的線程了,也就是說此時 maximumPoolSize 的設置就無效了,也無法觸發 RejectedExecutionHandler 拒絕策略,任務只會源源不斷地填充到 workQueue,直到 OOM。"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bd/bdbdb945e9e71dcb9a958a9c3917c955.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以 workQueue 應該爲有界隊列,至少保證在任務過載的情況下線程池還能正常工作,那麼哪些是有有界隊列,哪些是無界隊列呢。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有界隊列我們常用的以下兩個"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LinkedBlockingQueue: 鏈表構成的有界隊列,按先進先出(FIFO)的順序對元素進行排列,但注意在創建時需指定其大小,否則其大小默認爲 Integer.MAX_VALUE,相當於無界隊列了"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ArrayBlockingQueue: 數組實現的有界隊列,按先進先出(FIFO)的順序對元素進行排列。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無界隊列我們常用 PriorityBlockingQueue 這個優先級隊列,任務插入的時候可以指定其權重以讓這些任務優先執行,但這個隊列很少用,原因很簡單,線程池裏的任務執行順序一般是平等的,如果真有必須某些類型的任務需要優先執行,大不了再開個線程池好了,將不同的任務類型用不同的線程池隔離開來,也是合理利用線程池的一種實踐。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到這我相信大家應該能回答開頭的問題「阿里 Java 代碼規範爲什麼不允許使用 Executors 快速創建線程池?」,最常見的是以下兩種創建方式"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/33/333d8ef0ede1fe1f57da295e81b74c20.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"newCachedThreadPool 方法的最大線程數設置成了 Integer.MAX_VALUE,而 newSingleThreadExecutor 方法創建 workQueue 時 LinkedBlockingQueue 未聲明大小,相當於創建了無界隊列,一不小心就會導致 OOM。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"threadFactory 如何設置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般業務中會有多個線程池,如果某個線程池出現了問題,定位是哪一個線程出問題很重要,所以爲每個線程池取一個名字就很有必要了,我司用的 dubbo 的 NamedThreadFactory 來生成 threadFactory,創建很簡單"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的實現還是很巧妙的,有興趣地可以看看它的源碼,每調用一次,底層有個計數器會加一,會依次命名爲 「demo-work-thread-1」, 「demo-work-thread-2」, 「demo-work-thread-3」這樣遞增的字符串。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實際的業務場景中,一般很難確定 corePoolSize, workQueue,maximumPoolSize 的大小,如果出問題了,一般來說只能重新設置一下這些參數再發布,這樣往往需要耗費一些時間,美團的這篇文章給出了讓人眼前一亮的解決方案,當發現問題(線程池監控告警)時,動態調整這些參數,可以讓這些參數實時生效,能在發現問題時及時解決,確實是個很好的思路。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"線程池提交任務的兩種方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程池創建好了,該怎麼給它提交任務,有兩種方式,調用 execute 和 submit 方法,來看下這兩個方法的方法簽名"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"// 方式一:execute 方法"}]},{"type":"codeinline","content":[{"type":"text","text":"public void execute(Runnable command) {"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"// 方式二:ExecutorService 中 submit 的三個方法"}]},{"type":"codeinline","content":[{"type":"text","text":" Future submit(Callable task);"}]},{"type":"codeinline","content":[{"type":"text","text":" Future submit(Runnable task, T result);"}]},{"type":"codeinline","content":[{"type":"text","text":"Future> submit(Runnable task);"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"區別在於調用 execute 無返回值,而調用  submit 可以返回 Future,那麼這個 Future 能到底能幹啥呢,看它的接口"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"public interface Future {"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    /**"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 取消正在執行的任務,如果任務已執行或已被取消,或者由於某些原因不能取消則返回 false"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 如果任務未開始或者任務已開始但可以中斷(mayInterruptIfRunning 爲 true),則"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 可以取消/中斷此任務"}]},{"type":"codeinline","content":[{"type":"text","text":"     */"}]},{"type":"codeinline","content":[{"type":"text","text":"    boolean cancel(boolean mayInterruptIfRunning);"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    /**"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 任務在完成前是否已被取消"}]},{"type":"codeinline","content":[{"type":"text","text":"     */"}]},{"type":"codeinline","content":[{"type":"text","text":"    boolean isCancelled();"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    /**"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 正常的執行完流程流程,或拋出異常,或取消導致的任務完成都會返回 true"}]},{"type":"codeinline","content":[{"type":"text","text":"     */"}]},{"type":"codeinline","content":[{"type":"text","text":"    boolean isDone();"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    /**"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 阻塞等待任務的執行結果"}]},{"type":"codeinline","content":[{"type":"text","text":"     */"}]},{"type":"codeinline","content":[{"type":"text","text":"    V get() throws InterruptedException, ExecutionException;"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    /**"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 阻塞等待任務的執行結果,不過這裏指定了時間,如果在 timeout 時間內任務還未執行完成,"}]},{"type":"codeinline","content":[{"type":"text","text":"     * 則拋出 TimeoutException 異常"}]},{"type":"codeinline","content":[{"type":"text","text":"     */"}]},{"type":"codeinline","content":[{"type":"text","text":"    V get(long timeout, TimeUnit unit)"}]},{"type":"codeinline","content":[{"type":"text","text":"        throws InterruptedException, ExecutionException, TimeoutException;"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以用 Future 取消任務,判斷任務是否已取消/完成,甚至可以阻塞等待結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"submit 爲啥能提交任務(Runnable)的同時也能返回任務(Future)的執行結果呢"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/80228bedf9ac9f03a15db0f6ee161626.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原來在最後執行 execute 前用 newTaskFor 將 task 封裝成了 RunnableFuture,newTaskFor 返回了 FutureTask 這個類,結構圖如下"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4b/4b9993ce4d5257361ab3ceef60fd73f5.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到 FutureTask 這個接口既實現了 Runnable 接口,也實現 Future 接口,所以在提交任務的同時也能利用 Future 接口來執行任務的取消,獲取任務的狀態,等待執行結果這些操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"execute 與 submit 除了是否能返回執行結果這一區別外,還有一個重要區別,那就是使用 execute 執行如果發生了異常,是捕獲不到的,默認會執行 ThreadGroup 的 uncaughtException 方法(下圖數字 2 對應的邏輯)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5a/5aa921c0b7cdb9defef9b981ecfcce60.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以如果你想監控執行 execute 方法時發生的異常,需要通過 threadFactory 來指定一個 UncaughtExceptionHandler,這樣就會執行上圖中的 1,進而執行 UncaughtExceptionHandler 中的邏輯,如下所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"//1.實現一個自己的線程池工廠"}]},{"type":"codeinline","content":[{"type":"text","text":"ThreadFactory factory = (Runnable r) -> {"}]},{"type":"codeinline","content":[{"type":"text","text":"    //創建一個線程"}]},{"type":"codeinline","content":[{"type":"text","text":"    Thread t = new Thread(r);"}]},{"type":"codeinline","content":[{"type":"text","text":"    //給創建的線程設置UncaughtExceptionHandler對象 裏面實現異常的默認邏輯"}]},{"type":"codeinline","content":[{"type":"text","text":"    t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> {"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 在此設置統計監控邏輯"}]},{"type":"codeinline","content":[{"type":"text","text":"        System.out.println(\"線程工廠設置的exceptionHandler\" + e.getMessage());"}]},{"type":"codeinline","content":[{"type":"text","text":"    });"}]},{"type":"codeinline","content":[{"type":"text","text":"    return t;"}]},{"type":"codeinline","content":[{"type":"text","text":"};"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"// 2.創建一個自己定義的線程池,使用自己定義的線程工廠"}]},{"type":"codeinline","content":[{"type":"text","text":"ExecutorService service = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10),factory);"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"//3.提交任務"}]},{"type":"codeinline","content":[{"type":"text","text":"service.execute(()->{"}]},{"type":"codeinline","content":[{"type":"text","text":"    int i=1/0;"}]},{"type":"codeinline","content":[{"type":"text","text":"});"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行以上邏輯最終會輸出「線程工廠設置的exceptionHandler/ by zero」,通過這樣的方式就能通過設定的 defaultUncaughtExceptionHandler 來執行我們的監控邏輯了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果用 submit ,如何捕獲異常呢,當我們調用 future.get 就可以捕獲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Callable testCallable = xxx;"}]},{"type":"codeinline","content":[{"type":"text","text":"Future future = executor.submit(myCallable);"}]},{"type":"codeinline","content":[{"type":"text","text":"try {"}]},{"type":"codeinline","content":[{"type":"text","text":"    future1.get(3));"}]},{"type":"codeinline","content":[{"type":"text","text":"} catch (InterruptedException e) {"}]},{"type":"codeinline","content":[{"type":"text","text":"    e.printStackTrace();"}]},{"type":"codeinline","content":[{"type":"text","text":"} catch (ExecutionException e) {"}]},{"type":"codeinline","content":[{"type":"text","text":"    e.printStackTrace();"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼 future 爲啥在 get 的時候才捕獲異步呢,因爲在執行 submit 時拋出異常後此異常被保存了起來,而在 get 的時候才被拋出"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/76/76d7907683806431f1182ecb58279136.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於 execute 和 submit 的執行流程 why 神的這篇文章寫得非常透徹,我就不拾人牙慧了,建議大家好好品品,收穫會很大!"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"ThreadPoolExecutor 源碼剖析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面鋪墊了這麼多,終於到了最核心的源碼剖析環節了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於線程池來說,我們最關心的是它的「狀態」和「可運行的線程數量」,一般來說我們可以選擇用兩個變量來記錄,不過 Doug Lea 只用了一個變量(ctl)就達成目的了,我們知道變量越多,代碼的可維護性就越差,也越容易出 bug, 所以只用一個變量就達成了兩個變量的效果,這讓代碼的可維護性大大提高,那麼他是怎麼設計的呢"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"// ThreadPoolExecutor.java"}]},{"type":"codeinline","content":[{"type":"text","text":"public class ThreadPoolExecutor extends AbstractExecutorService {"}]},{"type":"codeinline","content":[{"type":"text","text":"    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));"}]},{"type":"codeinline","content":[{"type":"text","text":"    private static final int COUNT_BITS = Integer.SIZE - 3;"}]},{"type":"codeinline","content":[{"type":"text","text":"    private static final int CAPACITY   = (1 <=0"}]},{"type":"codeinline","content":[{"type":"text","text":"            setState(-1); "}]},{"type":"codeinline","content":[{"type":"text","text":"            this.firstTask = firstTask;"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"            // 根據線程池的 threadFactory 創建一個線程,將 worker 本身傳給線程(因爲 worker 實現了 Runnable 接口)"}]},{"type":"codeinline","content":[{"type":"text","text":"            this.thread = getThreadFactory().newThread(this);"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        public void run() {"}]},{"type":"codeinline","content":[{"type":"text","text":"            // thread 啓動後會調用此方法"}]},{"type":"codeinline","content":[{"type":"text","text":"            runWorker(this);"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"       "}]},{"type":"codeinline","content":[{"type":"text","text":"        // 1 代表被鎖住了,0 代表未鎖"}]},{"type":"codeinline","content":[{"type":"text","text":"        protected boolean isHeldExclusively() {"}]},{"type":"codeinline","content":[{"type":"text","text":"            return getState() != 0;"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // 嘗試獲取鎖"}]},{"type":"codeinline","content":[{"type":"text","text":"        protected boolean tryAcquire(int unused) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 從這裏可以看出它是一個獨佔鎖,因爲當獲取鎖後,cas 設置 state 不可能成功,這裏我們也能明白上文中將 state 設置爲 -1 的作用,這種情況下永遠不可能獲取得鎖,而 worker 要被中斷首先必須獲取鎖"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (compareAndSetState(0, 1)) {"}]},{"type":"codeinline","content":[{"type":"text","text":"                setExclusiveOwnerThread(Thread.currentThread());"}]},{"type":"codeinline","content":[{"type":"text","text":"                return true;"}]},{"type":"codeinline","content":[{"type":"text","text":"            }"}]},{"type":"codeinline","content":[{"type":"text","text":"            return false;"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // 嘗試釋放鎖"}]},{"type":"codeinline","content":[{"type":"text","text":"        protected boolean tryRelease(int unused) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            setExclusiveOwnerThread(null);"}]},{"type":"codeinline","content":[{"type":"text","text":"            setState(0);"}]},{"type":"codeinline","content":[{"type":"text","text":"            return true;"}]},{"type":"codeinline","content":[{"type":"text","text":"        }    "}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        public void lock()        { acquire(1); }"}]},{"type":"codeinline","content":[{"type":"text","text":"        public boolean tryLock()  { return tryAcquire(1); }"}]},{"type":"codeinline","content":[{"type":"text","text":"        public void unlock()      { release(1); }"}]},{"type":"codeinline","content":[{"type":"text","text":"        public boolean isLocked() { return isHeldExclusively(); }"}]},{"type":"codeinline","content":[{"type":"text","text":"            "}]},{"type":"codeinline","content":[{"type":"text","text":"        // 中斷線程,這個方法會被 shutdowNow 調用,從中可以看出 shutdownNow 要中斷線程不需要獲取鎖,也就是說如果線程正在運行,照樣會給你中斷掉,所以一般來說我們不用 shutdowNow 來中斷線程,太粗暴了,中斷時線程很可能在執行任務,影響任務執行"}]},{"type":"codeinline","content":[{"type":"text","text":"        void interruptIfStarted() {"}]},{"type":"codeinline","content":[{"type":"text","text":"            Thread t;"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 中斷也是有條件的,必須是 state >= 0 且 t != null 且線程未被中斷"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 如果 state == -1 ,不執行中斷,再次明白了爲啥上文中 setState(-1) 的意義"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {"}]},{"type":"codeinline","content":[{"type":"text","text":"                try {"}]},{"type":"codeinline","content":[{"type":"text","text":"                    t.interrupt();"}]},{"type":"codeinline","content":[{"type":"text","text":"                } catch (SecurityException ignore) {"}]},{"type":"codeinline","content":[{"type":"text","text":"                }"}]},{"type":"codeinline","content":[{"type":"text","text":"            }"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline","content":[{"type":"text","text":"    }"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上文對 Worker 類的分析,相信大家不難理解 "},{"type":"text","marks":[{"type":"strong"}],"text":"將線程封裝爲 worker 主要是爲了更好地管理線程的中斷"},{"type":"text","text":" 這句話。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"理解了 Worker 的意義,我們再來看 addWorker 的方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"private boolean addWorker(Runnable firstTask, boolean core) {"}]},{"type":"codeinline","content":[{"type":"text","text":"    retry:"}]},{"type":"codeinline","content":[{"type":"text","text":"    for (;;) {"}]},{"type":"codeinline","content":[{"type":"text","text":"        int c = ctl.get();"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // 獲取線程池的狀態"}]},{"type":"codeinline","content":[{"type":"text","text":"        int rs = runStateOf(c);"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // 如果線程池的狀態 >= SHUTDOWN,即爲 SHUTDOWN,STOP,TIDYING,TERMINATED 這四個狀態,只有一種情況有可能創建線程,即線程狀態爲 SHUTDOWN, 且隊列非空時,firstTask == null 代表創建一個不接收新任務的線程(此線程會從 workQueue 中獲取任務再執行),這種情況下創建線程是爲了加速處理完 workQueue 中的任務"}]},{"type":"codeinline","content":[{"type":"text","text":"        if (rs >= SHUTDOWN &&"}]},{"type":"codeinline","content":[{"type":"text","text":"            ! (rs == SHUTDOWN &&"}]},{"type":"codeinline","content":[{"type":"text","text":"               firstTask == null &&"}]},{"type":"codeinline","content":[{"type":"text","text":"               ! workQueue.isEmpty()))"}]},{"type":"codeinline","content":[{"type":"text","text":"            return false;"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        for (;;) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 獲取線程數"}]},{"type":"codeinline","content":[{"type":"text","text":"            int wc = workerCountOf(c);"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 如果超過了線程池的最大 CAPACITY(5 億多,基本不可能)"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 或者 超過了 corePoolSize(core 爲 true) 或者 maximumPoolSize(core 爲 false) 時"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 則返回 false"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (wc >= CAPACITY ||"}]},{"type":"codeinline","content":[{"type":"text","text":"                wc >= (core ? corePoolSize : maximumPoolSize))"}]},{"type":"codeinline","content":[{"type":"text","text":"                return false;"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 否則 CAS 增加線程的數量,如果成功跳出雙重循環"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (compareAndIncrementWorkerCount(c))"}]},{"type":"codeinline","content":[{"type":"text","text":"                break retry;"}]},{"type":"codeinline","content":[{"type":"text","text":"            c = ctl.get();  // Re-read ctl"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"            // 如果線程運行狀態發生變化,跳到外層循環繼續執行"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (runStateOf(c) != rs)"}]},{"type":"codeinline","content":[{"type":"text","text":"                continue retry;"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 說明是因爲 CAS 增加線程數量失敗所致,繼續執行 retry 的內層循環"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline","content":[{"type":"text","text":"    }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    boolean workerStarted = false;"}]},{"type":"codeinline","content":[{"type":"text","text":"    boolean workerAdded = false;"}]},{"type":"codeinline","content":[{"type":"text","text":"    Worker w = null;"}]},{"type":"codeinline","content":[{"type":"text","text":"    try {"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 能執行到這裏,說明滿足增加 worker 的條件了,所以創建 worker,準備添加進線程池中執行任務"}]},{"type":"codeinline","content":[{"type":"text","text":"        w = new Worker(firstTask);"}]},{"type":"codeinline","content":[{"type":"text","text":"        final Thread t = w.thread;"}]},{"type":"codeinline","content":[{"type":"text","text":"        if (t != null) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 加鎖,是因爲下文要把 w 添加進 workers 中, workers 是 HashSet,不是線程安全的,所以需要加鎖予以保證"}]},{"type":"codeinline","content":[{"type":"text","text":"            final ReentrantLock mainLock = this.mainLock;"}]},{"type":"codeinline","content":[{"type":"text","text":"            mainLock.lock();"}]},{"type":"codeinline","content":[{"type":"text","text":"            try {"}]},{"type":"codeinline","content":[{"type":"text","text":"                //  再次 check 線程池的狀態以防執行到此步時發生中斷等"}]},{"type":"codeinline","content":[{"type":"text","text":"                int rs = runStateOf(ctl.get());"}]},{"type":"codeinline","content":[{"type":"text","text":"                // 如果線程池狀態小於 SHUTDOWN(即爲 RUNNING),"}]},{"type":"codeinline","content":[{"type":"text","text":"                // 或者狀態爲 SHUTDOWN 但 firstTask == null(代表不接收任務,只是創建線程處理 workQueue 中的任務),則滿足添加 worker 的條件"}]},{"type":"codeinline","content":[{"type":"text","text":"                if (rs  largestPoolSize)"}]},{"type":"codeinline","content":[{"type":"text","text":"                        largestPoolSize = s;"}]},{"type":"codeinline","content":[{"type":"text","text":"                    workerAdded = true;"}]},{"type":"codeinline","content":[{"type":"text","text":"                }"}]},{"type":"codeinline","content":[{"type":"text","text":"            } finally {"}]},{"type":"codeinline","content":[{"type":"text","text":"                mainLock.unlock();"}]},{"type":"codeinline","content":[{"type":"text","text":"            }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"            // 說明往 workers 中添加 worker 成功,此時啓動線程"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (workerAdded) {"}]},{"type":"codeinline","content":[{"type":"text","text":"                t.start();"}]},{"type":"codeinline","content":[{"type":"text","text":"                workerStarted = true;"}]},{"type":"codeinline","content":[{"type":"text","text":"            }"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline","content":[{"type":"text","text":"    } finally {"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 添加線程失敗,執行 addWorkerFailed 方法,主要做了將 worker 從 workers 中移除,減少線程數,並嘗試着關閉線程池這樣的操作"}]},{"type":"codeinline","content":[{"type":"text","text":"        if (! workerStarted)"}]},{"type":"codeinline","content":[{"type":"text","text":"            addWorkerFailed(w);"}]},{"type":"codeinline","content":[{"type":"text","text":"    }"}]},{"type":"codeinline","content":[{"type":"text","text":"    return workerStarted;"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從這段代碼我們可以看到多線程下情況的不可預料性,我們發現在滿足條件情況下,又對線程狀態重新進行了 check,以防期間出現中斷等線程池狀態發生變更的操作,這也給我們以啓發:多線程環境下的各種臨界條件一定要考慮到位。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行 addWorker 創建 worker 成功後,線程開始執行了(t.start()),由於在創建 Worker 時,將 Worker  自己傳給了此線程,所以啓動線程後,會調用  Worker 的 run 方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"public void run() {"}]},{"type":"codeinline","content":[{"type":"text","text":"    runWorker(this);"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到最終會調用  runWorker 方法,接下來我們來分析下 runWorker 方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"final void runWorker(Worker w) {"}]},{"type":"codeinline","content":[{"type":"text","text":"    Thread wt = Thread.currentThread();"}]},{"type":"codeinline","content":[{"type":"text","text":"    Runnable task = w.firstTask;"}]},{"type":"codeinline","content":[{"type":"text","text":"    w.firstTask = null;"}]},{"type":"codeinline","content":[{"type":"text","text":"    // unlock 會調用 tryRelease 方法將 state 設置成 0,代表允許中斷,允許中斷的條件上文我們在 interruptIfStarted() 中有提過,即 state >= 0"}]},{"type":"codeinline","content":[{"type":"text","text":"    w.unlock();"}]},{"type":"codeinline","content":[{"type":"text","text":"    boolean completedAbruptly = true;"}]},{"type":"codeinline","content":[{"type":"text","text":"    try {"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 如果在提交任務時創建了線程,並把任務丟給此線程,則會先執行此 task"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 否則從任務隊列中獲取 task 來執行(即 getTask() 方法)"}]},{"type":"codeinline","content":[{"type":"text","text":"        while (task != null || (task = getTask()) != null) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            w.lock();"}]},{"type":"codeinline","content":[{"type":"text","text":"            "}]},{"type":"codeinline","content":[{"type":"text","text":"            // 如果線程池狀態爲 >= STOP(即 STOP,TIDYING,TERMINATED )時,則線程應該中斷"}]},{"type":"codeinline","content":[{"type":"text","text":"            // 如果線程池狀態 = min)"}]},{"type":"codeinline","content":[{"type":"text","text":"                return; // replacement not needed"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline","content":[{"type":"text","text":"        addWorker(null, false);"}]},{"type":"codeinline","content":[{"type":"text","text":"    }"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們分析 woker 從 workQueue 中取任務的方法 getTask"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"private Runnable getTask() {"}]},{"type":"codeinline","content":[{"type":"text","text":"    boolean timedOut = false; // Did the last poll() time out?"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"    for (;;) {"}]},{"type":"codeinline","content":[{"type":"text","text":"        int c = ctl.get();"}]},{"type":"codeinline","content":[{"type":"text","text":"        int rs = runStateOf(c);"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // 如果線程池狀態至少爲 STOP 或者"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 線程池狀態 == SHUTDOWN 並且任務隊列是空的"}]},{"type":"codeinline","content":[{"type":"text","text":"        // 則減少線程數量,返回 null,這種情況下上文分析的 runWorker 會執行 processWorkerExit 從而讓獲取此 Task 的 woker 退出"}]},{"type":"codeinline","content":[{"type":"text","text":"        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            decrementWorkerCount();"}]},{"type":"codeinline","content":[{"type":"text","text":"            return null;"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        int wc = workerCountOf(c);"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // 如果 allowCoreThreadTimeOut 爲 true,代表任何線程在 keepAliveTime 時間內處於 idle 狀態都會被回收,如果線程數大於 corePoolSize,本身在 keepAliveTime 時間內處於 idle 狀態就會被回收"}]},{"type":"codeinline","content":[{"type":"text","text":"        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        // worker 應該被回收的幾個條件,這個比較簡單,就此略過"}]},{"type":"codeinline","content":[{"type":"text","text":"        if ((wc > maximumPoolSize || (timed && timedOut))"}]},{"type":"codeinline","content":[{"type":"text","text":"            && (wc > 1 || workQueue.isEmpty())) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (compareAndDecrementWorkerCount(c))"}]},{"type":"codeinline","content":[{"type":"text","text":"                return null;"}]},{"type":"codeinline","content":[{"type":"text","text":"            continue;"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline"},{"type":"codeinline","content":[{"type":"text","text":"        try {"}]},{"type":"codeinline","content":[{"type":"text","text":"           // 阻塞獲取 task,如果在 keepAliveTime 時間內未獲取任務,說明超時了,此時 timedOut 爲 true"}]},{"type":"codeinline","content":[{"type":"text","text":"            Runnable r = timed ?"}]},{"type":"codeinline","content":[{"type":"text","text":"                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :"}]},{"type":"codeinline","content":[{"type":"text","text":"                workQueue.take();"}]},{"type":"codeinline","content":[{"type":"text","text":"            if (r != null)"}]},{"type":"codeinline","content":[{"type":"text","text":"                return r;"}]},{"type":"codeinline","content":[{"type":"text","text":"            timedOut = true;"}]},{"type":"codeinline","content":[{"type":"text","text":"        } catch (InterruptedException retry) {"}]},{"type":"codeinline","content":[{"type":"text","text":"            timedOut = false;"}]},{"type":"codeinline","content":[{"type":"text","text":"        }"}]},{"type":"codeinline","content":[{"type":"text","text":"    }"}]},{"type":"codeinline","content":[{"type":"text","text":"}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過以上源碼剖析,相信我們對線程池的工作原理了解得八九不離十了,再來簡單過一下其他一些比較有用的方法,開頭我們提到線程池的監控問題,我們看一下可以監控哪些指標"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int getCorePoolSize():獲取核心線程數。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int getLargestPoolSize():歷史峯值線程數。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int getMaximumPoolSize():最大線程數(線程池線程容量)。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int getActiveCount():當前活躍線程數"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"int getPoolSize():當前線程池中的線程總數"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"BlockingQueuegetQueue() 當前線程池的任務隊列,據此可以獲取積壓任務的總數,getQueue.size()"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"監控思路也很簡單,開啓一個定時線程 ScheduledThreadPoolExecutor,定期對這些線程池指標進行採集,一般會採用一些開源工具如 Grafana + Prometheus + MicroMeter 來實現。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何實現核心線程池的預熱"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用  prestartAllCoreThreads() 方法,這個方法會一次性創建 corePoolSize 個線程,無需等到提交任務時才創建,提交創建好線程的話,一有任務提交過來,這些線程就可以立即處理。"}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何實現動態調整線程池參數"}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setCorePoolSize(int corePoolSize) 調整核心線程池大小"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setMaximumPoolSize(int maximumPoolSize)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setKeepAliveTime() 設置線程的存活時間"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"解答開篇的問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其它問題基本都在源碼剖析環節回答了,這裏簡單說下其他問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、Tomcat 的線程池和 JDK 的線程池實現有啥區別, Dubbo 中有類似 Tomcat 的線程池實現嗎? Dubbo 中一個叫 EagerThreadPool 的東西,可以看看它的使用說明"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/66/66388ea4e0ed8ebe7673e78d86e45d12.png","alt":"","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從註釋裏可以看出,如果核心線程都處於 busy 狀態,如果有新的請求進來,EagerThreadPool 會選擇先創建線程,而不是將其放入任務隊列中,這樣可以更快地響應這些請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Tomcat 實現也是與此類似的,只不過稍微有所不同,當 Tomcat 啓動時,會先創建 minSpareThreads 個線程,如果經過一段時間收到請求時這些線程都處於忙碌狀態,每次都會以 minSpareThreads 的步長創建線程,本質上也是爲了更快地響應處理請求。具體的源碼可以看它的 ThreadPool 實現,這裏就不展開了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、我司網關 dubbo 調用線程池曾經出現過這樣的一個問題:壓測時接口可以正常返回,但接口 RT 很高,假設設置的核心線程大小爲 500,最大線程爲 800,緩衝隊列爲 5000,你能從這個設置中發現出一些問題並對這些參數進行調優嗎?這個參數明顯能看出問題來,首先任務隊列設置過大,任務達到核心線程後,如果再有請求進來會先進入任務隊列,隊列滿了之後才創建線程,創建線程也是需要不少開銷的,所以我們後來把核心線程設置成了與最大線程一樣,並且調用 prestartAllCoreThreads() 來預熱核心線程,就不用等請求來時再創建線程了。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"線程池的幾個最佳實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、線程池執行的任務應該是互相獨立的,如果互相依賴的話,可能導致死鎖,比如下面這樣的代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"ExecutorService pool = Executors"}]},{"type":"codeinline","content":[{"type":"text","text":"  .newSingleThreadExecutor();"}]},{"type":"codeinline","content":[{"type":"text","text":"pool.submit(() -> {"}]},{"type":"codeinline","content":[{"type":"text","text":"  try {"}]},{"type":"codeinline","content":[{"type":"text","text":"    String qq=pool.submit(()->\"QQ\").get();"}]},{"type":"codeinline","content":[{"type":"text","text":"    System.out.println(qq);"}]},{"type":"codeinline","content":[{"type":"text","text":"  } catch (Exception e) {"}]},{"type":"codeinline","content":[{"type":"text","text":"  }"}]},{"type":"codeinline","content":[{"type":"text","text":"});"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、核心任務與非核心任務最好能用多個線程池隔離開來"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"曾經我們業務上就出現這樣的一個故障:突然很多用戶反饋短信收不到了,排查才發現發短信是在一個線程池裏,而另外的定時腳本也是用的這個線程池來執行任務,這個腳本一分鐘可能產生幾百上千條任務,導致發短信的方法在線程池裏基本沒機會執行,後來我們用了兩個線程池把發短信和執行腳本隔離開來解決了問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、添加線程池監控,動態設置線程池"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如前文所述,線程池的各個參數很難一次性確定,既然難以確定,又要保證發現問題後及時解決,我們就需要爲線程池增加監控,監控隊列大小,線程數量等,我們可以設置 3 分鐘內比如隊列任務一直都是滿了的話,就觸發告警,這樣可以提前預警,如果線上因爲線程池參數設置不合理而觸發了降級等操作,可以通過動態設置線程池的方式來實時修改核心線程數,最大線程數等,將問題及時修復。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文詳細剖析了線程池的工作原理,相信大家對其工作機制應該有了較深入的瞭解,也對開頭的幾個問題有了較清楚的認識,本質上設置線程池的目的是爲了利用有效的資源最大化性能,最小化風險,同時線程池的使用本質上是爲了更好地爲用戶服務,據此也不難明白 Tomcat, Dubbo 要另起爐竈來設置自己的線程池了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後歡迎大家關注我的公衆號【Java鬥帝】,一起討論,共同進步,拉你進讀者羣,我們一起抱團取暖!"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"看完三件事❤️"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"========"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"點贊,轉發,有你們的 『點贊和評論』,纔是我創造的動力。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關注公衆號 『 Java鬥帝 』,不定期分享原創知識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時可以期待後續文章ing🚀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章