面試題 -- 如何設計一個線程池


以前,我總覺得的買一件東西,做一件事,或者從某一個時間節點開始,我的生命就會發生轉折,一切就會無比順利,立馬變厲害。但是,事實上並不是如此。我不可能馬上變厲害,也不可能一口吃成一個胖子。看一篇文章也不能讓你從此走上人生巔峰,越來越相信,這是一個長期的過程,只有量變引起質變,縱使緩慢,馳而不息。

如何設計一個線程池?

三個步驟

這是一個常見的問題,如果在比較熟悉線程池運作原理的情況下,這個問題並不難。設計實現一個東西,三步走:是什么?為什么?怎么做?

線程池是什么?

線程池使用了池化技術,將線程存儲起來放在一個 "池子"(容器)里面,來了任務可以用已有的空閑的線程進行處理, 處理完成之后,歸還到容器,可以復用。如果線程不夠,還可以根據規則動態增加,線程多余的時候,亦可以讓多余的線程死亡。

為什么要用線程池?

實現線程池有什么好處呢?

  • 降低資源消耗:池化技術可以重復利用已經創建的線程,降低線程創建和銷毀的損耗。
  • 提高響應速度:利用已經存在的線程進行處理,少去了創建線程的時間
  • 管理線程可控:線程是稀缺資源,不能無限創建,線程池可以做到統一分配和監控
  • 拓展其他功能:比如定時線程池,可以定時執行任務

需要考慮的點

那線程池設計需要考慮的點:

  • 線程池狀態:

    • 有哪些狀態?如何維護狀態?
  • 線程

    • 線程怎么封裝?線程放在哪個池子里?
    • 線程怎么取得任務?
    • 線程有哪些狀態?
    • 線程的數量怎么限制?動態變化?自動伸縮?
    • 線程怎么消亡?如何重復利用?
  • 任務

    • 任務少可以直接處理,多的時候,放在哪里?
    • 任務隊列滿了,怎么辦?
    • 用什么隊列?

如果從任務的階段來看,分為以下幾個階段:

  • 如何存任務?
  • 如何取任務?
  • 如何執行任務?
  • 如何拒絕任務?

線程池狀態

狀態有哪些?如何維護狀態?

狀態可以設置為以下幾種:

  • RUNNING:運行狀態,可以接受任務,也可以處理任務
  • SHUTDOWN:不可以接受任務,但是可以處理任務
  • STOP:不可以接受任務,也不可以處理任務,中斷當前任務
  • TIDYING:所有線程停止
  • TERMINATED:線程池的最后狀態

各種狀態之間是不一樣的,他們的狀態之間變化如下:

而維護狀態的話,可以用一個變量單獨存儲,並且需要保證修改時的原子性,在底層操作系統中,對int的修改是原子的,而在32位的操作系統里面,對double,long這種64位數值的操作不是原子的。除此之外,實際上JDK里面實現的狀態和線程池的線程數是同一個變量,高3位表示線程池的狀態,而低29位則表示線程的數量。

這樣設計的好處是節省空間,並且同時更新的時候有優勢。

線程相關

線程怎么封裝?線程放在哪個池子里?

線程,即是實現了Runnable接口,執行的時候,調用的是start()方法,但是start()方法內部編譯后調用的是 run() 方法,這個方法只能調用一次,調用多次會報錯。因此線程池里面的線程跑起來之后,不可能終止再啟動,只能一直運行着。既然不可以停止,那么執行完任務之后,沒有任務過來,只能是輪詢取出任務的過程

線程可以運行任務,因此封裝線程的時候,假設封裝成為 Worker, Worker里面必定是包含一個 Thread,表示當前線程,除了當前線程之外,封裝的線程類還應該持有任務,初始化可能直接給予任務,當前的任務是null的時候才需要去獲取任務。

可以考慮使用 HashSet 來存儲線程,也就是充當線程池的角色,當然,HashSet 會有線程安全的問題需要考慮,那么我們可以考慮使用一個可重入鎖比如 ReentrantLock,凡是增刪線程池的線程,都需要鎖住。

    private final ReentrantLock mainLock = new ReentrantLock();

線程怎么取得任務?

(1)初始化線程的時候可以直接指定任務,譬如Runnable firstTask,將任務封裝到 worker 中,然后獲取 worker 里面的 threadthread.run()的時候,其實就是 跑的是 worker 本身的 run() 方法,因為 worker 本身就是實現了 Runnable 接口,里面的線程其實就是其本身。因此也可以實現對 ThreadFactory 線程工廠的定制化。

    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        final Thread thread;
        Runnable firstTask;

        ...

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            // 從線程池創建線程,傳入的是其本身
            this.thread = getThreadFactory().newThread(this);
        }
    }

(2)運行完任務的線程,應該繼續取任務,取任務肯定需要從任務隊列里面取,要是任務隊列里面沒有任務,由於是阻塞隊列,那么可以等待,如果等待若干時間后,仍沒有任務,倘若該線程池的線程數已經超過核心線程數,並且允許線程消亡的話,應該將該線程從線程池中移除,並結束掉該線程。

取任務和執行任務,對於線程池里面的線程而言,就是一個周而復始的工作,除非它會消亡。

線程有哪些狀態?

現在我們所說的是Java中的線程Thread,一個線程在一個給定的時間點,只能處於一種狀態,這些狀態都是虛擬機的狀態,不能反映任何操作系統的線程狀態,一共有六種/七種狀態:

  • NEW:創建了線程對象,但是還沒有調用Start()方法,還沒有啟動的線程處於這種狀態。

  • Running:運行狀態,其實包含了兩種狀態,但是Java線程將就緒和運行中統稱為可運行

    • Runnable:就緒狀態:創建對象后,調用了start()方法,該狀態的線程還位於可運行線程池中,等待調度,獲取CPU的使用權
      • 只是有資格執行,不一定會執行
      • start()之后進入就緒狀態,sleep()結束或者join()結束,線程獲得對象鎖等都會進入該狀態。
      • CPU時間片結束或者主動調用yield()方法,也會進入該狀態
    • Running :獲取到CPU的使用權(獲得CPU時間片),變成運行中
  • BLOCKED :阻塞,線程阻塞於鎖,等待監視器鎖,一般是Synchronize關鍵字修飾的方法或者代碼塊

  • WAITING :進入該狀態,需要等待其他線程通知(notify)或者中斷,一個線程無限期地等待另一個線程。

  • TIMED_WAITING :超時等待,在指定時間后自動喚醒,返回,不會一直等待

  • TERMINATED :線程執行完畢,已經退出。如果已終止再調用start(),將會拋出java.lang.IllegalThreadStateException異常。

image-20210509224848865

線程的數量怎么限制?動態變化?自動伸縮?

線程池本身,就是為了限制和充分使用線程資的,因此有了兩個概念:核心線程數,最大線程數。

要想讓線程數根據任務數量動態變化,那么我們可以考慮以下設計(假設不斷有任務):

  • 來一個任務創建一個線程處理,直到線程數達到核心線程數。
  • 達到核心線程數之后且沒有空閑線程,來了任務直接放到任務隊列。
  • 任務隊列如果是無界的,會被撐爆。
  • 任務隊列如果是有界的,任務隊列滿了之后,還有任務過來,會繼續創建線程處理,此時線程數大於核心線程數,直到線程數等於最大線程數。
  • 達到最大線程數之后,還有任務不斷過來,會觸發拒絕策略,根據不同策略進行處理。
  • 如果任務不斷處理完成,任務隊列空了,線程空閑沒任務,會在一定時間內,銷毀,讓線程數保持在核心線程數即可。

由上面可以看出,主要控制伸縮的參數是核心線程數最大線程數,任務隊列,拒絕策略

線程怎么消亡?如何重復利用?

線程不能被重新調用多次start(),因此只能調用一次,也就是線程不可能停下來,再啟動。那么就說明線程復用只是在不斷的循環罷了。

消亡只是結束了它的run()方法,當線程池數量需要自動縮容的,就會讓一部分空閑的線程結束。

而重復利用,其實是執行完任務之后,再去去任務隊列取任務,取不到任務會等待,任務隊列是一個阻塞隊列,這是一個不斷循環的過程。

任務相關

任務少可以直接處理,多的時候,放在哪里?

任務少的時候,來了直接創建,賦予線程初始化任務,就可開始執行,任務多的時候,把它放進隊列里面,先進先出。

任務隊列滿了,怎么辦?

任務隊列滿了,會繼續增加線程,直到達到最大的線程數。

用什么隊列?

一般的隊列,只是一個有限長度的緩沖區,要是滿了,就不能保存當前的任務,阻塞隊列可以通過阻塞,保留出當前需要入隊的任務,只是會阻塞等待。同樣的,阻塞隊列也可以保證任務隊列沒有任務的時候,阻塞當前獲取任務的線程,讓它進入wait狀態,釋放cpu的資源。因此在線程池的場景下,阻塞隊列其實是比較有必要的。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。這個世界希望一切都很快,更快,但是我希望自己能走好每一步,寫好每一篇文章,期待和你們一起交流。如果有幫助,順手點個贊,對我,是莫大的鼓勵和認可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM