1. 程式人生 > >池化技術到達有多牛?看了執行緒和執行緒池的對比嚇我一跳!

池化技術到達有多牛?看了執行緒和執行緒池的對比嚇我一跳!

情商高的人是能洞察並照顧到身邊所有人的情緒,而好的文章應該是讓所有人都能看懂。 **尼采曾經說過:人們無法理解他沒有經歷過的事情**。因此我會試著把技術文章寫的儘量具象化一些,力求讓所有人都能看懂,所以在正式開始之前,我們先從兩個生活事例說起。 尼采帥照: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1595842242256-a6d548c0-229b-41e8-9158-d225f7857319.png) > 嘮嗑:之前一直以為尼采是中國的某位聖人,大體和莊子差不多,後來才知道原來是一位老外,驚了個呆。 ## 生活案例 1 早些年間,某寶雙“11”突然爆火,然後無數個男男女女瘋狂“剁手”,然而最痛苦的並不是“剁手”之後吃“灰”的日子,而是漫長而又揪心的等待快遞小哥的日子。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1595758964607-91a01b9e-d8a5-4825-b64f-1534bf9bff97.png) 為了緩解彼此的“痛苦”(快遞公司的電話被打爆,使用者等得不耐煩),快遞公司後面就變“聰明”了,每當購物節將要來臨之前,快遞公司會**預先準備好充足的人和車**,以迎接撲面而來的訂單。 至此,當我們再遇到各種購物節,就再也不用每天盯著手機煎熬的等待快遞小哥了。 ## 生活案例 2 小美是一家公司的 HR,每年年初是小美最頭疼的日子了。因為年初有大量的員工離職,因此小美需要一邊辦理離職員工的手續,一邊瘋狂的招人,除了這些工作之外,小美還要忍受來自各部門和大 BOSS 的間歇性催促,這些都讓小美痛苦不已。 於是為了應對每年年初的這種囧境,小美也變聰明瞭,她每年年末的時候都會**預先招聘一些員工**,以備來年的不時之需。 自從用了這招之後(提前招人),小美從此過上了幸福的生活。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1595760646717-aca169c8-df8d-472b-a4d9-d6dff9aa361d.png) ## 概念 **池化技術**指的是**提前準備**一些資源,在需要時可以**重複使用**這些預先準備的資源。 也就是說池化技術有兩個優點: 1. 提前建立; 1. 重複利用。 ## 池化技術優點分析 以 Java 中的物件建立來說,在物件建立時要經歷以下步驟: 1. 根據 new 識別符號後面的引數,在常量池查詢類的符號引用; 1. 如果沒找到符號應用(類並未載入),進行類的載入、解析、初始化等; 1. 虛擬機器為物件在堆中分配記憶體,並將分配的記憶體初始化為 0,針對物件頭,建立相應的描述結構(耗時操作:需要查詢堆中的空閒區域,修改記憶體分配狀態等); 1. 呼叫物件的初始化方法(耗時操作:使用者的複雜的邏輯驗證等操作,如IO、數值計算是否符合規定等)。 從上述的流程中可以看出,建立一個類需要經歷複雜且耗時的操作,因此**我們應該儘量複用已有的類,以確保程式的高效執行,當然如果能夠提前建立這些類就再好不過了,而這些功能都可以用池化技術來實現**。 ## 池化技術常見應用 常見的池化技術的使用有:執行緒池、記憶體池、資料庫連線池、HttpClient 連線池等,下面分別來看。 ### 1.執行緒池 執行緒池的原理很簡單,類似於作業系統中的緩衝區的概念。執行緒池中會先啟動若干數量的執行緒,這些執行緒都處於睡眠狀態。當客戶端有一個新的請求時,就會喚醒執行緒池中的某一個睡眠的執行緒,讓它來處理客戶端的這個請求,當處理完這個請求之後,執行緒又處於睡眠的狀態。 執行緒池能很高地提升程式的效能。比如有一個省級資料大集中的銀行網路中心,高峰期每秒的客戶端請求併發數超過100,如果為每個客戶端請求建立一個新的執行緒的話,那耗費的 CPU 時間和記憶體都是十分驚人的,如果採用一個擁有 200 個執行緒的執行緒池,那將會節約大量的系統資源,使得更多的 CPU 時間和記憶體用來處理實際的商業應用,而不是頻繁的執行緒建立和銷燬。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1595845224538-5582a385-2735-4c1f-b6e3-2350b473f0fc.png) ### 2.記憶體池 如何更好地管理應用程式記憶體的使用,同時提高記憶體使用的頻率,這時值得每一個開發人員深思的問題。記憶體池(Memory Pool)就提供了一個比較可行的解決方案。 記憶體池在建立的過程中,會預先分配足夠大的記憶體,形成一個初步的記憶體池。然後每次使用者請求記憶體的時候,就會返回記憶體池中的一塊空閒的記憶體,並將這塊記憶體的標誌置為已使用。當記憶體使用完畢釋放記憶體的時候,也不是真正地呼叫 free 或 delete 的過程,而是把記憶體放回記憶體池的過程,且放回的過程要把標誌置為空閒。最後,應用程式結束就會將記憶體池銷燬,將記憶體池中的每一塊記憶體釋放。 **記憶體池的優點**: - 減少記憶體碎片的產生,這個優點可以從建立記憶體池的過程中看出,當我們在建立記憶體池的時候,分配的都是一塊塊比較規整的記憶體塊,減少記憶體碎片的產生。 - 提高了記憶體的使用頻率。這個可以從分配記憶體和釋放記憶體的過程中看出。每次的分配和釋放並不是去呼叫系統提供的函式或操作符去操作實際的記憶體,而是在複用記憶體池中的記憶體。 **記憶體池的缺點**:會造成記憶體的浪費,因為要使用記憶體池需要在一開始分配一大塊閒置的記憶體,而這些記憶體不一定全部被用到。 ### 3.資料庫連線池 資料庫連線池的基本思想是在系統初始化的時候將資料庫連線作為物件儲存在記憶體中,當用戶需要訪問資料庫的時候,並非建立一個新的連線,而是從連線池中取出一個已建立的空閒連線物件。在使用完畢後,使用者也不是將連線關閉,而是將連線放回到連線池中,以供下一個請求訪問使用,而這些連線的建立、斷開都是由連線池自身來管理的。 同時,還可以設定連線池的引數來控制連線池中的初始連線數、連線的上下限數和每個連線的最大使用次數、最大空閒時間等。當然,也可以通過連線池自身的管理機制來監視連線的數量、使用情況等。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1595845192506-ce56b397-806d-4fab-995a-75194fbc712e.png) ### 4.HttpClient 連線池 HttpClient 我們經常用來進行 HTTP 服務訪問。我們的專案中會有一個獲取任務執行狀態的功能使用 HttpClient,一秒鐘請求一次,經常會出現 Conection Reset 異常。經過分析發現,問題是出在 HttpClient 的每次請求都會新建一個連線,當建立連線的頻率比關閉連線的頻率大的時候,就會導致系統中產生大量處於 TIME_CLOSED 狀態的連線,這個時候使用連線池複用連線就能解決這個問題。 ## 實戰:執行緒 VS 執行緒池 本文我們使用之前文章介紹的統計方法[《6種快速統計程式碼執行時間的方法,真香!(史上最全)》](https://mp.weixin.qq.com/s/e5UeSfygPUWf49AtD0RgMQ),來測試一下執行緒和執行緒池執行的時間差距有多大,測試程式碼如下: ```java import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 執行緒池 vs 執行緒 效能對比 */ public class ThreadPoolPerformance { // 最大執行次數 public static final int maxCount = 1000; public static void main(String[] args) throws InterruptedException { // 執行緒測試程式碼 ThreadPerformanceTest(); // 執行緒池測試程式碼 ThreadPoolPerformanceTest(); } /** * 執行緒池效能測試 */ private static void ThreadPoolPerformanceTest() throws InterruptedException { // 開始時間 long stime = System.currentTimeMillis(); // 業務程式碼 ThreadPoolExecutor tp = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>()); for (int i = 0; i < maxCount; i++) { tp.execute(new PerformanceRunnable()); } tp.shutdown(); tp.awaitTermination(1, TimeUnit.SECONDS); // 等待執行緒池執行完成 // 結束時間 long etime = System.currentTimeMillis(); // 計算執行時間 System.out.printf("執行緒池執行時長:%d 毫秒.", (etime - stime)); System.out.println(); } /** * 執行緒效能測試 */ private static void ThreadPerformanceTest() { // 開始時間 long stime = System.currentTimeMillis(); // 執行業務程式碼 for (int i = 0; i < maxCount; i++) { Thread td = new Thread(new PerformanceRunnable()); td.start(); try { td.join(); // 確保執行緒執行完成 } catch (InterruptedException e) { e.printStackTrace(); } } // 結束時間 long etime = System.currentTimeMillis(); // 計算執行時間 System.out.printf("執行緒執行時長:%d 毫秒.", (etime - stime)); System.out.println(); } // 業務執行類 static class PerformanceRunnable implements Runnable { @Override public void run() { for (int i = 0; i < maxCount; i++) { long num = i * i + i; } } } } ``` 以上程式的執行結果如下圖所示: ![執行緒.gif](https://cdn.nlark.com/yuque/0/2020/gif/92791/1595848163830-0231a008-9c7d-49d4-be3a-1a9914448666.gif) 為了防止執行的先後順序影響測試結果,下面我將執行緒池和執行緒呼叫方法打個顛倒,執行結果如下圖所示: ![執行緒池.gif](https://cdn.nlark.com/yuque/0/2020/gif/92791/1595848172791-83294476-c89d-4875-9ec2-8a42c38fa340.gif) ## 總結 從執行緒和執行緒池的測試結果來看,當我們使用池化技術時,程式的效能可以提升 10 倍。此測試結果並不代表池化技術的效能量化結果,因為測試結果受執行方法和迴圈次數的影響,但**巨大的效能差異足以說明池化技術的優勢所在**。 無獨有偶,阿里巴巴的《Java開發手冊》中也強制規定「**執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒**」規定如下: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1595865127660-d9d5b533-992f-4b39-accc-6922af86360f.png) 因此掌握並使用池化技術是一個合格程式設計師的標配,你還知道哪些常用的池化技術嗎?歡迎評論區留言補充。 #### 參考 & 引用 https://zhuanlan.zhihu.com/p/32204303 https://www.cnblogs.com/yanggb/p/106323