尼採曾經說過:人們無法理解他沒有經歷過的事情。因此我會試着把技術文章寫的儘量具象化一些,力求讓所有人都能看懂,所以在正式開始之前,我們先從兩個生活事例說起。
嘮嗑:之前一直以爲尼採是中國的某位聖人,大體和莊子差不多,後來才知道原來是一位老外,驚了個呆。
早些年間,某寶雙「11」突然爆火,然後無數個男男女女瘋狂「剁手」,然而最痛苦的並不是「剁手」之後吃「灰」的日子,而是漫長而又揪心的等待快遞小哥的日子。
爲了緩解彼此的「痛苦」(快遞公司的電話被打爆,使用者等得不耐煩),快遞公司後面就變「聰明」了,每當購物節將要來臨之前,快遞公司會預先準備好充足的人和車,以迎接撲面而來的訂單。
至此,當我們再遇到各種購物節,就再也不用每天盯着手機煎熬的等待快遞小哥了。
小美是一家公司的 HR,每年年初是小美最頭疼的日子了。因爲年初有大量的員工離職,因此小美需要一邊辦理離職員工的手續,一邊瘋狂的招人,除了這些工作之外,小美還要忍受來自各部門和大 BOSS 的間歇性催促,這些都讓小美痛苦不已。
於是爲了應對每年年初的這種囧境,小美也變聰明瞭,她每年年末的時候都會預先招聘一些員工,以備來年的不時之需。
自從用了這招之後(提前招人),小美從此過上了幸福的生活。
池化技術指的是提前準備一些資源,在需要時可以重複使用這些預先準備的資源。
也就是說池化技術有兩個優點:
以 Java 中的物件建立來說,在物件建立時要經歷以下步驟:
從上述的流程中可以看出,建立一個類需要經歷複雜且耗時的操作,因此我們應該儘量複用已有的類,以確保程式的高效執行,當然如果能夠提前建立這些類就再好不過了,而這些功能都可以用池化技術來實現。
常見的池化技術的使用有:執行緒池、記憶體池、數據庫連線池、HttpClient 連線池等,下面 下麪分別來看。
執行緒池的原理很簡單,類似於操作系統中的緩衝區的概念。執行緒池中會先啓動若幹數量的執行緒,這些執行緒都處於睡眠狀態。當用戶端有一個新的請求時,就會喚醒執行緒池中的某一個睡眠的執行緒,讓它來處理用戶端的這個請求,當處理完這個請求之後,執行緒又處於睡眠的狀態。
執行緒池能很高地提升程式的效能。比如有一個省級數據大集中的銀行網路中心,高峯期每秒的用戶端請求併發數超過100,如果爲每個用戶端請求建立一個新的執行緒的話,那耗費的 CPU 時間和記憶體都是十分驚人的,如果採用一個擁有 200 個執行緒的執行緒池,那將會節約大量的系統資源,使得更多的 CPU 時間和記憶體用來處理實際的商業應用,而不是頻繁的執行緒建立和銷燬。
如何更好地管理應用程式記憶體的使用,同時提高記憶體使用的頻率,這時值得每一個開發人員深思的問題。記憶體池(Memory Pool)就提供了一個比較可行的解決方案。
記憶體池在建立的過程中,會預先分配足夠大的記憶體,形成一個初步的記憶體池。然後每次使用者請求記憶體的時候,就會返回記憶體池中的一塊空閒的記憶體,並將這塊記憶體的標誌置爲已使用。當記憶體使用完畢釋放記憶體的時候,也不是真正地呼叫 free 或 delete 的過程,而是把記憶體放回記憶體池的過程,且放回的過程要把標誌置爲空閒。最後,應用程式結束就會將記憶體池銷燬,將記憶體池中的每一塊記憶體釋放。
記憶體池的優點:
記憶體池的缺點:會造成記憶體的浪費,因爲要使用記憶體池需要在一開始分配一大塊閒置的記憶體,而這些記憶體不一定全部被用到。
數據庫連線池的基本思想是在系統初始化的時候將數據庫連線作爲物件儲存在記憶體中,當使用者需要存取數據庫的時候,並非建立一個新的連線,而是從連線池中取出一個已建立的空閒連線物件。在使用完畢後,使用者也不是將連線關閉,而是將連線放回到連線池中,以供下一個請求存取使用,而這些連線的建立、斷開都是由連線池自身來管理的。
同時,還可以設定連線池的參數來控制連線池中的初始連線數、連線的上下限數和每個連線的最大使用次數、最大空閒時間等。當然,也可以通過連線池自身的管理機制 機製來監視連線的數量、使用情況等。
HttpClient 我們經常用來進行 HTTP 服務存取。我們的專案中會有一個獲取任務執行狀態的功能使用 HttpClient,一秒鐘請求一次,經常會出現 Conection Reset 異常。經過分析發現,問題是出在 HttpClient 的每次請求都會新建一個連線,當建立連線的頻率比關閉連線的頻率大的時候,就會導致系統中產生大量處於 TIME_CLOSED 狀態的連線,這個時候使用連線池複用連線就能解決這個問題。
本文我們使用之前文章介紹的統計方法《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開發手冊》中也強制規定「執行緒資源必須通過執行緒池提供,不允許在應用中自行顯式建立執行緒」規定如下:
因此掌握並使用池化技術是一個合格程式設計師的標配,你還知道哪些常用的池化技術嗎?歡迎評論區留言補充。