情商高的人是能洞察并照顧到身邊所有人的情緒,而好的文章應該是讓所有人都能看懂。
尼采曾經說過:人們無法了解他沒有經曆過的事情。是以我會試着把技術文章寫的盡量具象化一些,力求讓所有人都能看懂,是以在正式開始之前,我們先從兩個生活事例說起。

唠嗑:之前一直以為尼采是中國的某位聖人,大體和莊子差不多,後來才知道原來是一位老外,驚了個呆。
生活案例 1
早些年間,某寶雙“11”突然爆火,然後無數個男男女女瘋狂“剁手”,然而最痛苦的并不是“剁手”之後吃“灰”的日子,而是漫長而又揪心的等待快遞小哥的日子。
為了緩解彼此的“痛苦”(快遞公司的電話被打爆,使用者等得不耐煩),快遞公司後面就變“聰明”了,每當購物節将要來臨之前,快遞公司會預先準備好充足的人和車,以迎接撲面而來的訂單。
至此,當我們再遇到各種購物節,就再也不用每天盯着手機煎熬的等待快遞小哥了。
生活案例 2
小美是一家公司的 HR,每年年初是小美最頭疼的日子了。因為年初有大量的員工離職,是以小美需要一邊辦理離職員工的手續,一邊瘋狂的招人,除了這些工作之外,小美還要忍受來自各部門和大 BOSS 的間歇性催促,這些都讓小美痛苦不已。
于是為了應對每年年初的這種囧境,小美也變聰明了,她每年年末的時候都會預先招聘一些員工,以備來年的不時之需。
自從用了這招之後(提前招人),小美從此過上了幸福的生活。
概念
池化技術指的是提前準備一些資源,在需要時可以重複使用這些預先準備的資源。
也就是說池化技術有兩個優點:
- 提前建立;
- 重複利用。
池化技術優點分析
以 Java 中的對象建立來說,在對象建立時要經曆以下步驟:
- 根據 new 辨別符後面的參數,在常量池查找類的符号引用;
- 如果沒找到符号應用(類并未加載),進行類的加載、解析、初始化等;
- 虛拟機為對象在堆中配置設定記憶體,并将配置設定的記憶體初始化為 0,針對對象頭,建立相應的描述結構(耗時操作:需要查找堆中的空閑區域,修改記憶體配置設定狀态等);
- 調用對象的初始化方法(耗時操作:使用者的複雜的邏輯驗證等操作,如IO、數值計算是否符合規定等)。
從上述的流程中可以看出,建立一個類需要經曆複雜且耗時的操作,是以我們應該盡量複用已有的類,以確定程式的高效運作,當然如果能夠提前建立這些類就再好不過了,而這些功能都可以用池化技術來實作。
池化技術常見應用
常見的池化技術的使用有:線程池、記憶體池、資料庫連接配接池、HttpClient 連接配接池等,下面分别來看。
1.線程池
線程池的原理很簡單,類似于作業系統中的緩沖區的概念。線程池中會先啟動若幹數量的線程,這些線程都處于睡眠狀态。當用戶端有一個新的請求時,就會喚醒線程池中的某一個睡眠的線程,讓它來處理用戶端的這個請求,當處理完這個請求之後,線程又處于睡眠的狀态。
線程池能很高地提升程式的性能。比如有一個省級資料大集中的銀行網絡中心,高峰期每秒的用戶端請求并發數超過100,如果為每個用戶端請求建立一個新的線程的話,那耗費的 CPU 時間和記憶體都是十分驚人的,如果采用一個擁有 200 個線程的線程池,那将會節約大量的系統資源,使得更多的 CPU 時間和記憶體用來處理實際的商業應用,而不是頻繁的線程建立和銷毀。
2.記憶體池
如何更好地管理應用程式記憶體的使用,同時提高記憶體使用的頻率,這時值得每一個開發人員深思的問題。記憶體池(Memory Pool)就提供了一個比較可行的解決方案。
記憶體池在建立的過程中,會預先配置設定足夠大的記憶體,形成一個初步的記憶體池。然後每次使用者請求記憶體的時候,就會傳回記憶體池中的一塊空閑的記憶體,并将這塊記憶體的标志置為已使用。當記憶體使用完畢釋放記憶體的時候,也不是真正地調用 free 或 delete 的過程,而是把記憶體放回記憶體池的過程,且放回的過程要把标志置為空閑。最後,應用程式結束就會将記憶體池銷毀,将記憶體池中的每一塊記憶體釋放。
記憶體池的優點:
- 減少記憶體碎片的産生,這個優點可以從建立記憶體池的過程中看出,當我們在建立記憶體池的時候,配置設定的都是一塊塊比較規整的記憶體塊,減少記憶體碎片的産生。
- 提高了記憶體的使用頻率。這個可以從配置設定記憶體和釋放記憶體的過程中看出。每次的配置設定和釋放并不是去調用系統提供的函數或操作符去操作實際的記憶體,而是在複用記憶體池中的記憶體。
記憶體池的缺點:會造成記憶體的浪費,因為要使用記憶體池需要在一開始配置設定一大塊閑置的記憶體,而這些記憶體不一定全部被用到。
3.資料庫連接配接池
資料庫連接配接池的基本思想是在系統初始化的時候将資料庫連接配接作為對象存儲在記憶體中,當使用者需要通路資料庫的時候,并非建立一個新的連接配接,而是從連接配接池中取出一個已建立的空閑連接配接對象。在使用完畢後,使用者也不是将連接配接關閉,而是将連接配接放回到連接配接池中,以供下一個請求通路使用,而這些連接配接的建立、斷開都是由連接配接池自身來管理的。
同時,還可以設定連接配接池的參數來控制連接配接池中的初始連接配接數、連接配接的上下限數和每個連接配接的最大使用次數、最大空閑時間等。當然,也可以通過連接配接池自身的管理機制來監視連接配接的數量、使用情況等。
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;
}
}
}
}
以上程式的執行結果如下圖所示:
為了防止執行的先後順序影響測試結果,下面我将線程池和線程調用方法打個颠倒,執行結果如下圖所示:
總結
從線程和線程池的測試結果來看,當我們使用池化技術時,程式的性能可以提升 10 倍。此測試結果并不代表池化技術的性能量化結果,因為測試結果受執行方法和循環次數的影響,但巨大的性能差異足以說明池化技術的優勢所在。
無獨有偶,阿裡巴巴的《Java開發手冊》中也強制規定「線程資源必須通過線程池提供,不允許在應用中自行顯式建立線程」規定如下:
是以掌握并使用池化技術是一個合格程式員的标配,你還知道哪些常用的池化技術嗎?歡迎評論區留言補充。