[014]你想自己設計一個線程池么

背景

作為幾年工作經(jīng)驗的java程序員肯定知道java中通過線程池來調(diào)度線程的。線程池分為幾種,為什么會設計這幾種線程池各自的實現(xiàn)算法是什么,適用場景是什么?這些疑問其實是脫離java語言,其他語言設計線程池也會遇到同樣的問題。所以這里對這線程池設計原理需要考慮的方面進行分析。

線程池的作用

1.線程池即預先創(chuàng)建線程的技術,一個線程執(zhí)行完后重新放回不會銷毀掉提高了線程的利用率。
2.由于我們要使用線程來執(zhí)行任務的時候直接從線程池中去現(xiàn)成的所以提高了程序的相應速度。
3.線程池可以對里面的線程進行管理,至于如何管理XXXX(如何銷毀線程、如何結束線程狀態(tài)等等)。

創(chuàng)建線程池需要考慮的

從這里我們知道線程池的一些基本配置參數(shù)。比如 線程池的大小,執(zhí)行任務隊列,線程池滿了新任務的執(zhí)行策略,工作線程空閑后存活時間(如果想提高線程利用率提議調(diào)大該時間)。
所以它的構造函數(shù)為:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) 

這里為什么有一個corePoolSize和maximumPoolSize呢? 級別關系是 coreSize -> 隊列 ->(無法使用隊列則創(chuàng)建新線程) maximumPoolSize。

a).corePoolSize意思是基本大小,比如線程池corePoolSize=10,而此時線程池里有5個線程且都是空閑的,由于還沒有達到corePoolSize,如果提交一個任務會從線程池里選擇一個線程來執(zhí)行任務。當達到了corePoolSize時Executor默認會先把任務添加進隊列中,如果無法加入隊列則創(chuàng)建新線程直到達到maximumPoolSize。
b).maximumPoolSize使用場景,如果線程池里的線程數(shù)量達到了maximumPoolSize且其中的線程沒有空閑的。當新任務到達的時候會新建線程,如果無限地創(chuàng)建會消耗系統(tǒng)的資源,所以這里有一個maximumPoolSize參數(shù),當線程數(shù)量達到maximumPoolSize的時候即時沒有空閑線程了也不會重新創(chuàng)建線程。

不重新創(chuàng)建線程那怎么辦呢?這就需要使用RejectedExecutionHandler(飽和策略)?,F(xiàn)有的飽和策略有,策略分兩種執(zhí)行與不執(zhí)行:
對于不執(zhí)行的,我可能會有以下情況:a.丟棄 b.拋出異常 c.丟棄但是記錄日志或持久化到數(shù)據(jù)庫(通過實現(xiàn)RejectedExecutionHandler接口來處理)。

對于執(zhí)行該任務會有如下的情況:a.騰出空間,替換最老未執(zhí)行的任務。
1).丟棄該任務 2).丟棄最老未執(zhí)行的騰出空間執(zhí)行該任務

加入隊列的幾種情況

當我們創(chuàng)建線程池需要指定隊列的時候必須,而不同隊列線程池會有不同的表現(xiàn)。
有3種常見的隊列:
a).ArrayBlockingQueue 有界隊列,創(chuàng)建時候必須制定大小(構造函數(shù)要求制定)
b).LinkedBlockingQueue 無界隊列

 public LinkedBlockingQueue() {
 }

c).SynchronousQueue 同步隊列,每新增一個任務的線程必須等待另一個線程取出任務。 //還是不是很理解同步隊列怎么實現(xiàn)的背后的實現(xiàn)原理-怎么做到同步的。
這3種隊列的使用場景是什么?
當資源有限的時候使用有界隊列,使用有界隊列的過程中,隊列大小和最大池大小可能需要互相折衷。大隊列小線程池大小可以降低CPU使用率和線程之間的切換。
使用無界隊列時候maxSize參數(shù)無用,因為當線程數(shù)超過coresize的時候會一直不停的往LinkedBlockingQueue里放。這個可以用于web服務器訪問量突發(fā)的情況。

線程池如何處理任務

這里講線程池如何提交任務,任務提交后如何跟蹤結果。
execute方式提交,這里沒有返回結果。所以無法獲取任務執(zhí)行結果。

public void execute(Runnable command) {
    ......
    addWorke(command,true)
    }

private boolean addWorker(Runnable firstTask, boolean core) {
     w = new Worker(firstTask); //這里會把Runable接口包裝城worker接口
     works.add(w);
}

*addWorker 怎么判斷線程池已經(jīng)滿了涉及到二進制操作,以后專門寫博客來闡述。

submit()方式提交可以通過future獲取任務執(zhí)行結果,當調(diào)用future.get()時候如果任務執(zhí)行未完成則會阻塞。

<T> Future<T> submit(Callable<T> task);

線程池如何關閉

線程池關閉的時候需要考慮其所處的狀態(tài),即如果有任務未執(zhí)行完怎么辦?什么時候應該關閉線程池。
常見的辦法就是一個個遍歷線程,如果不等待執(zhí)行完就sotp停止線程或者中斷現(xiàn)在執(zhí)行的線程。
線程池關閉的狀態(tài)中有幾個中間狀態(tài)可以根據(jù) 隊列只否有正在執(zhí)行的線程,有的話是否繼續(xù)執(zhí)行來劃分。
線程池的狀態(tài)有:Running 可以接收新的任務和執(zhí)行隊列任務,shutdown 不接收新的任務和已有隊列任務還需要執(zhí)行,stop 不接收新任務且 已有隊列任務也停止(interrupt in-process task),terminate 線程池已經(jīng)停止了
這里shutdown()與shutdownNow的區(qū)別就是shutdown只會interrputIdleWork,即只會終端沒有非運行時的線程,正在執(zhí)行的線程等待執(zhí)行完。
代碼區(qū)別如下:

 public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

使用篇

前面介紹了線程池的基本功能,這里就對其如何使用進行分析。
使用涉及到配置、啟動、狀態(tài)監(jiān)控

線程池的配置選型

即對各種類型的任務使用什么樣的策略。我暫時想到的任務類型劃分標準有,1.執(zhí)行時間長短 2.優(yōu)先級 3.cpu型的還是IO型的 4.任務是否依賴其他特性

原則是對于CPU秘籍型的任務,線程池內(nèi)的線程不宜過多避免頻繁切換,可以設置為 N cpu+1
對于IO密集型的任務,線程池內(nèi)的線程可以設置為2*N cpu

對于優(yōu)先級可以使用PriorityBlockingQueue隊列,但是如果一直有高優(yōu)先級的任務那么低優(yōu)先級的任務永遠執(zhí)行不了。

對于執(zhí)行時間過長的(比如數(shù)據(jù)庫)需要一定時間才能返回所以空閑時間比較長,這樣的話可以把線程數(shù)量設置大一些。

線程池的監(jiān)控

我們想監(jiān)控線程池所有線程是否執(zhí)行完,線程池里的線程使用狀態(tài),已經(jīng)完成的線程數(shù)。
ThreadPoolExecutor提供了一些變量來存儲線程池的狀態(tài),比如taskCount(線程池),completedTaskCount,largestPoolSize(曾經(jīng)創(chuàng)建過的最大線程數(shù))。

public class ThreadPoolExecutor extends AbstractExecutorService {
       private int largestPoolSize;
       private long completedTaskCount;
}

寫完后的想法

1.通過從線程池的維度主動檢索其知識來理解execute(),submit()方法,這種方式層次清晰,在信息維度就是從高緯往低維度去找是一種好的方式。
2.任何框架的描述都可以自己提出很多的問題, 原理、結構、如何使用等等,通過這些提問來掌握知識是一種很好的辦法,從另一角度來說你能夠提出多少問題你對這一領域抽象層次就了解多少。
3.至于各種線程池還沒有闡述分析等待下一篇吧。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容