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

情商高的人是能洞察並照顧到身邊所有人的情緒,而好的文章應該是讓所有人都能看懂。

尼采曾經說過:人們無法理解他沒有經歷過的事情。因此我會試著把技術文章寫的盡量具象化一些,力求讓所有人都能看懂,所以在正式開始之前,我們先從兩個生活事例說起。

尼采帥照:
image.png

嘮嗑:之前一直以為尼采是中國的某位聖人,大體和莊子差不多,後來才知道原來是一位老外,驚了個呆。

生活案例 1

早些年間,某寶雙「11」突然爆火,然後無數個男男女女瘋狂「剁手」,然而最痛苦的並不是「剁手」之後吃「灰」的日子,而是漫長而又揪心的等待快遞小哥的日子。
image.png

為了緩解彼此的「痛苦」(快遞公司的電話被打爆,用戶等得不耐煩),快遞公司後面就變「聰明」了,每當購物節將要來臨之前,快遞公司會預先準備好充足的人和車,以迎接撲面而來的訂單。

至此,當我們再遇到各種購物節,就再也不用每天盯著手機煎熬的等待快遞小哥了。

生活案例 2

小美是一家公司的 HR,每年年初是小美最頭疼的日子了。因為年初有大量的員工離職,因此小美需要一邊辦理離職員工的手續,一邊瘋狂的招人,除了這些工作之外,小美還要忍受來自各部門和大 BOSS 的間歇性催促,這些都讓小美痛苦不已。

於是為了應對每年年初的這種囧境,小美也變聰明了,她每年年末的時候都會預先招聘一些員工,以備來年的不時之需。

自從用了這招之後(提前招人),小美從此過上了幸福的生活。
image.png

概念

池化技術指的是提前準備一些資源,在需要時可以重複使用這些預先準備的資源。

也就是說池化技術有兩個優點:

  1. 提前創建;
  2. 重複利用。

池化技術優點分析

以 Java 中的對象創建來說,在對象創建時要經歷以下步驟:

  1. 根據 new 標識符後面的參數,在常量池查找類的符號引用;
  2. 如果沒找到符號應用(類並未載入),進行類的載入、解析、初始化等;
  3. 虛擬機為對象在堆中分配記憶體,並將分配的記憶體初始化為 0,針對對象頭,建立相應的描述結構(耗時操作:需要查找堆中的空閑區域,修改記憶體分配狀態等);
  4. 調用對象的初始化方法(耗時操作:用戶的複雜的邏輯驗證等操作,如IO、數值計算是否符合規定等)。

從上述的流程中可以看出,創建一個類需要經歷複雜且耗時的操作,因此我們應該盡量復用已有的類,以確保程式的高效運行,當然如果能夠提前創建這些類就再好不過了,而這些功能都可以用池化技術來實現

池化技術常見應用

常見的池化技術的使用有:執行緒池、記憶體池、資料庫連接池、HttpClient 連接池等,下面分別來看。

1.執行緒池

執行緒池的原理很簡單,類似於作業系統中的緩衝區的概念。執行緒池中會先啟動若干數量的執行緒,這些執行緒都處於睡眠狀態。當客戶端有一個新的請求時,就會喚醒執行緒池中的某一個睡眠的執行緒,讓它來處理客戶端的這個請求,當處理完這個請求之後,執行緒又處於睡眠的狀態。

執行緒池能很高地提升程式的性能。比如有一個省級數據大集中的銀行網路中心,高峰期每秒的客戶端請求並發數超過100,如果為每個客戶端請求創建一個新的執行緒的話,那耗費的 CPU 時間和記憶體都是十分驚人的,如果採用一個擁有 200 個執行緒的執行緒池,那將會節約大量的系統資源,使得更多的 CPU 時間和記憶體用來處理實際的商業應用,而不是頻繁的執行緒創建和銷毀。

image.png

2.記憶體池

如何更好地管理應用程式記憶體的使用,同時提高記憶體使用的頻率,這時值得每一個開發人員深思的問題。記憶體池(Memory Pool)就提供了一個比較可行的解決方案。

記憶體池在創建的過程中,會預先分配足夠大的記憶體,形成一個初步的記憶體池。然後每次用戶請求記憶體的時候,就會返回記憶體池中的一塊空閑的記憶體,並將這塊記憶體的標誌置為已使用。當記憶體使用完畢釋放記憶體的時候,也不是真正地調用 free 或 delete 的過程,而是把記憶體放回記憶體池的過程,且放回的過程要把標誌置為空閑。最後,應用程式結束就會將記憶體池銷毀,將記憶體池中的每一塊記憶體釋放。

記憶體池的優點

  • 減少記憶體碎片的產生,這個優點可以從創建記憶體池的過程中看出,當我們在創建記憶體池的時候,分配的都是一塊塊比較規整的記憶體塊,減少記憶體碎片的產生。
  • 提高了記憶體的使用頻率。這個可以從分配記憶體和釋放記憶體的過程中看出。每次的分配和釋放並不是去調用系統提供的函數或操作符去操作實際的記憶體,而是在復用記憶體池中的記憶體。

記憶體池的缺點:會造成記憶體的浪費,因為要使用記憶體池需要在一開始分配一大塊閑置的記憶體,而這些記憶體不一定全部被用到。

3.資料庫連接池

資料庫連接池的基本思想是在系統初始化的時候將資料庫連接作為對象存儲在記憶體中,當用戶需要訪問資料庫的時候,並非建立一個新的連接,而是從連接池中取出一個已建立的空閑連接對象。在使用完畢後,用戶也不是將連接關閉,而是將連接放回到連接池中,以供下一個請求訪問使用,而這些連接的建立、斷開都是由連接池自身來管理的。

同時,還可以設置連接池的參數來控制連接池中的初始連接數、連接的上下限數和每個連接的最大使用次數、最大空閑時間等。當然,也可以通過連接池自身的管理機制來監視連接的數量、使用情況等。

image.png

4.HttpClient 連接池

HttpClient 我們經常用來進行 HTTP 服務訪問。我們的項目中會有一個獲取任務執行狀態的功能使用 HttpClient,一秒鐘請求一次,經常會出現 Conection Reset 異常。經過分析發現,問題是出在 HttpClient 的每次請求都會新建一個連接,當創建連接的頻率比關閉連接的頻率大的時候,就會導致系統中產生大量處於 TIME_CLOSED 狀態的連接,這個時候使用連接池復用連接就能解決這個問題。

實戰:執行緒 VS 執行緒池

本文我們使用之前文章介紹的統計方法《6種快速統計程式碼執行時間的方法,真香!(史上最全)》,來測試一下執行緒和執行緒池執行的時間差距有多大,測試程式碼如下:

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
為了防止執行的先後順序影響測試結果,下面我將執行緒池和執行緒調用方法打個顛倒,執行結果如下圖所示:
執行緒池.gif

總結

從執行緒和執行緒池的測試結果來看,當我們使用池化技術時,程式的性能可以提升 10 倍。此測試結果並不代表池化技術的性能量化結果,因為測試結果受執行方法和循環次數的影響,但巨大的性能差異足以說明池化技術的優勢所在

無獨有偶,阿里巴巴的《Java開發手冊》中也強制規定「執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式創建執行緒」規定如下:
image.png
因此掌握並使用池化技術是一個合格程式設計師的標配,你還知道哪些常用的池化技術嗎?歡迎評論區留言補充。

參考 & 引用

//zhuanlan.zhihu.com/p/32204303

//www.cnblogs.com/yanggb/p/10632317.html

Tags: