Jdk默認線程池弱爆了 ?

看到好玩的、想了解的,記錄一下

一、ThreadExecutor 線程池

1.1 ThreadExecutor 線程池執行邏輯

從execute 方法的註釋清晰得知,傳統線程加入線程池執行過程分3步

  • 小於等於Coresize: 創建線程之行
  • 大於CoreSize 加入隊列
  • 隊列滿且小於maxSize 有空閒線程使用空閒線程執行,沒有的話,創建線程執行
  • 大於maxSize 拒絕策略執行
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
        * Proceed in 3 steps:
        *
        * 1. If fewer than corePoolSize threads are running, try to
        * start a new thread with the given command as its first
        * task.  The call to addWorker atomically checks runState and
        * workerCount, and so prevents false alarms that would add
        * threads when it shouldn't, by returning false.
        *
        * 2. If a task can be successfully queued, then we still need
        * to double-check whether we should have added a thread
        * (because existing ones died since last checking) or that
        * the pool shut down since entry into this method. So we
        * recheck state and if necessary roll back the enqueuing if
        * stopped, or start a new thread if there are none.
        *
        * 3. If we cannot queue task, then we try to add a new
        * thread.  If it fails, we know we are shut down or saturated
        * and so reject the task.
        */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

1.2 ThreadPoolExecutor 優點:

優先offer到queue,queue滿後再擴充線程到maxThread,如果已經到了maxThread就reject 比較適合於CPU密集型應用(比如runnable內部執行的操作都在JVM內部,memory copy, or compute等等)
java.util.concurrent.ThreadPoolExecutor#execute

1.3 corePoolSize、maximumPoolSize

corePoolSize和maximumPoolSize是ThreadPoolExecutor核心的兩個參數,分別表示了核心線程數和最大線程數,corePoolSize一般設置爲cpu核數或者核數的兩倍,達到最大化利用cpu的效果,maximumPoolSize則通過設置最大線程數防止線程數暴漲影響系統運行。這個兩個參數非常的重要,根據上面的理解,在結合阿里規約瞭解一下爲啥禁止直接使用Executors,這裏面最核心的問題就出在Executors對workQueue的處理上,在常用的newFixedThreadPool 和 newCachedThreadPool 兩個創建方式上,newFixedThreadPool採用了一個不限容量的隊列(new LinkedBlockingQueue(),而newCachedThreadPool採用了一個阻塞隊列,結果就是在突發流量的請求下,newFixedThreadPool的線程數會保持corePoolSize大小,將來不及處理的請求壓入等待隊列,而newCachedThreadPool則選擇創建不受限制的新線程去執行突發的請求。從系統安全性考慮,這兩種方式在面對大量突發請求的情況下都將出現系統資源佔用甚至耗盡問題,而從業務指標上來講,排隊和線程切換等問題的存在,這兩種機制在吞吐和延遲上都無法作出有效的優化。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());

二、Tomcat StandardThreadExecutor

2.1 繼續 corePoolSize、maximumPoolSize

核心線程數和最大線程數的使用規則?爲啥JDK不是直接使用corePoolSize滿了直接增加maximumPoolSize 然後在入隊呢?ThreadPoolExecutor 比較適合於CPU密集型應用(memory copy, or compute),這個也是設計的初衷,但是副作用就是會犧牲系統的吞吐和延遲(ThreadPoolExecutor的實現的話,我們就可以看到ThreadPoolExecutor實現邏輯是先排隊再擴線程,如果隊列是個不限容量隊列,那麼超過corePoolSize的請求都將直接排隊,maximumPoolSize的設置也將完全不起作用。這樣設計的理由是儘量不去創建新線程複用現有線程,副作用就是會犧牲系統的吞吐和延遲。)

2.2 先maximumPoolSize 再Queue(StandardThreadExecutor)

Tomcat的核心線程池實現類是StandardThreadExecutor,該類封裝了對ThreadPoolExecutor(tomcat其實自己基於jdk的ThreadPoolExecutor封裝了自己的ThreadPoolExecutor實現,StandardThreadExecutor最大的特點是實現了一個繼承自LinkedBlockingQueue的定長隊列TaskQueue,巧妙的使用了Queue的特性,通過重載LinkedBlockingQueue的offer方法,在觸發排隊的情況下,如果線程池中線程數小於最大線程數限制,則直接通過返回false的方式拒絕請求,強制線程池創建新的線程去處理請求,而只有線程數達到最大線程數限制之後才接受排隊請求。


org.apache.catalina.core.StandardThreadExecutor#startInternal

    // TaskQueue extends LinkedBlockingQueue<Runnable>
    taskqueue = new TaskQueue(maxQueueSize);
    TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
    executor.setThreadRenewalDelay(threadRenewalDelay);
    if (prestartminSpareThreads) {
        executor.prestartAllCoreThreads();
    }
    taskqueue.setParent(executor);

2.2.1 突破隊列限制、重寫入隊規則

org.apache.tomcat.util.threads.TaskQueue 結合當前線程池的數量進行控制,在執行java.util.concurrent.ThreadPoolExecutor#execute 插入隊列失敗,導致強制進入創建線程池處理,這樣破了JDK默認的策略。

@Override
public boolean offer(Runnable o) {
    //we can't do any checks
    if (parent==null) return super.offer(o);
    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

2.3 StandardThreadExecutor execute 執行優點:

StandardThreadExecutor 優先擴充線程到maxThread,再offer到queue,如果滿了就reject,比較適合於業務處理需要遠程資源的場景(請求處理時間存在限制,但同時希望保證可控的響應延遲,那麼Tomcat的模式似乎更值得考慮一下)

三、如何選擇

3.1 名詞

如何合理的設置線程池ThreadPoolExecutor的大小

CPU密集型

儘量使用較小的線程池,一般Cpu核心數+1 。
因爲CPU密集型任務CPU的使用率很高,若開過多的線程,只能增加線程上下文的切換次數,帶來額外的開銷。

IO密集型

方法一:可以使用較大的線程池,一般CPU核心數 * 2
IO密集型CPU使用率不高,可以讓CPU等待IO的時候處理別的任務,充分利用cpu時間。


方法二:線程等待時間所佔比例越高,需要越多線程。線程CPU時間所佔比例越高,需要越少線程。


下面舉個例子:
比如平均每個線程CPU運行時間爲0.5s,而線程等待時間(非CPU運行時間,比如IO)爲1.5s,CPU核心數爲8,那麼根據上面這個公式估算得到:((0.5+1.5)/0.5)8=32。這個公式進一步轉化爲:


最佳線程數目 = (線程等待時間與線程CPU時間之比 + 1) CPU數目

3.2 如何選擇呢?

線程池的初衷就是爲了更好的性能,通過分析業務的使用場景,選擇合適的線程池就顯得非常重要了,在方案的選擇上,除了要考慮核心線程數和最大線程數的設置,線程池的調度邏輯也是需要考慮的因素之一。一般情況下使用JDK默認的基本滿足需求啦,對於響應要求很高的,可以考慮一下Tomact線程池的設計,突破常規。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章