記一次生產問題--CompletableFuture預設執行緒池
在jdk7中,我們使用執行緒池可能會使用ExecutorService,預設有四種方式
Executors.newSingleeThreadPool()
Executors.newFixedThreadPool()
Executors.newCacheThreadPool()
Executors.newScheduledThreadPool()
在jdk8中,CompletableFuture騰空出世,它簡化了非同步任務的寫法,提供了很多非同步任務的計算方式。
言歸正傳,現在生產上面出現的問題是,在流量激增的時候,響應很慢,慢慢的所有請求都無妨得到響應結果。
解決方案:
①檢視cpu和記憶體使用率
cpu使用率很低,5%左右,記憶體使用一直不變,基本排除不是他們的問題。
①.檢視gc
看到full gc沒有發生,young gc 雖然增加了一點,但是平均響應時間也就是50ms,也算正常了。
② 分析dump堆
下了一個126M的dump包,有一個類佔了60m。首先懷疑是不是記憶體溢位了,但是通過分析,這部分快取是必需要做的,並且當時dump下來的包確實有點小,資料觀測的不夠準確。
③.檢視棧資訊
發現有很多ForkJoinPool.commonPool-worker-執行緒正在等待,其實使用過CompletableFuture的同學就知道,它裡面用的是ForkJoin池來實現的。有想了解執行緒池原始碼的可以去讀讀這篇文章。
為什麼這裡會有這麼多的執行緒在等待呢?生產上面的伺服器使用的是一個兩核的伺服器,執行緒池裡面只會是1個執行緒可以執行。為什麼是一個,請看原始碼。
@Test public void test12() throws InterruptedException { 先做一個單元測試 CompletableFuture.runAsync(()->{ //在此處打斷點 System.out.println("111"); }); Thread.sleep(400000); }
一步一步把程式碼貼出來,看官看好。
public static CompletableFuture<Void> runAsync(Runnable runnable) { //執行執行緒的方法 return asyncRunStage(asyncPool, runnable); }
asyncPool是什麼?看一下這個值的設定。
private static final Executor asyncPool = useCommonPool ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
useCommonPool是什麼?
private static final boolean useCommonPool = (ForkJoinPool.getCommonPoolParallelism() > 1);
public static int getCommonPoolParallelism() { return commonParallelism; }
commonParallelism就是那個併發的執行緒數,它又是怎麼來的呢?
static { // initialize field offsets for CAS etc 。。。。。。 commonMaxSpares = DEFAULT_COMMON_MAX_SPARES; defaultForkJoinWorkerThreadFactory = new DefaultForkJoinWorkerThreadFactory(); modifyThreadPermission = new RuntimePermission("modifyThread"); common = java.security.AccessController.doPrivileged (new java.security.PrivilegedAction<ForkJoinPool>() { public ForkJoinPool run() { return makeCommonPool(); }}); //重點看makeCommonPool方法 int par = common.config & SMASK; // 獲取到par SMASK的值是 65535 也就是1111111111111111 &操作還是common.config本身,看樣子還是要看看config是怎麼來的 commonParallelism = par > 0 ? par : 1; 想知道par是什麼值,這個值為負數預設是1 }
private static ForkJoinPool makeCommonPool() { int parallelism = -1; //這個併發的執行緒數預設是-1 ForkJoinWorkerThreadFactory factory = null; 。。。。。。 if (parallelism < 0 && (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0) //看到了吧,執行緒池中的處理執行緒數=電腦核數-1 parallelism = 1; if (parallelism > MAX_CAP) parallelism = MAX_CAP; return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE, "ForkJoinPool.commonPool-worker-"); //指定執行緒的名字 }
到此分析完畢,使用了逆推法。
由於生產伺服器電腦核數較小,而在CompletableFuture程式碼中又使用了預設的執行緒池,處理的執行緒個數是電腦核數-1。這樣等有大請求量過來,處理邏輯又很複雜,很多執行緒都在等待執行,慢慢拖垮了伺服器。
調整執行緒池的大小
《Java併發程式設計實戰》(http://mng.bz/979c)一書中,Brian Goetz和合著者們為執行緒池大小的優化提供了不少中肯的建議。這非常重要,如果執行緒池中執行緒的數量過多,最終它們會競爭
稀缺的處理器和記憶體資源,浪費大量的時間在上下文切換上。反之,如果執行緒的數目過少,正
如你的應用所面臨的情況,處理器的一些核可能就無法充分利用。Brian Goetz建議,執行緒池大
小與處理器的利用率之比可以使用下面的公式進行估算:
N threads = N CPU * U CPU * (1 + W/C)
其中:
❑N CPU 是處理器的核的數目,可以通過 Runtime.getRuntime().availableProce-
ssors() 得到
❑U CPU 是期望的CPU利用率(該值應該介於0和1之間)
❑W/C是等待時間與計算時間的比率
這裡太囉嗦了,一般的設定執行緒池的大小規則是
如果服務是cpu密集型的,設定為電腦的核數
如果服務是io密集型的,設定為電腦的核數*2