Java實習生面試複習(十):線程池ThreadPoolExecutor學習

我是一名很普通的大三學生。我會堅持寫博客,輸出知識的同時鞏固自己的基礎,記錄自己的成長和鍛鍊自己,奧利給!!

如果你覺得內容對你有幫助的話,不如給個贊鼓勵一下更新😂?(σ゚∀゚)σ…:*☆哎喲不錯哦

生活從來都是公平的,你未來的模樣,藏在你現在的努力裏。

線程池

線程池是什麼,好處是啥?

線程池是爲了避免線程頻繁的創建和銷燬帶來的性能消耗,而建立的一種池化技術,它是把已創建的線程放入“池”中,當有任務來臨時就可以重用已有的線程,無需等待創建的過程,這樣就可以有效提高程序的響應速度。
通過線程池複用線程有以下幾點優點:

  • 減少資源創建 => 減少內存開銷,創建線程佔用內存
  • 降低系統開銷 => 創建線程需要時間,會延遲處理的請求
  • 提高穩定穩定性 => 避免無限創建線程引起的OutOfMemoryError【簡稱OOM】

在阿里巴巴的《Java 開發手冊》中是這樣規定線程池的

  • 【強制】線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。 說明:線程池的好處是減少在創建和銷燬線程上所消耗的時間以及系統資源的開銷,解決資源不足的問題。 如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。
  • 【強制】線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這 樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
    說明:Executors返回的線程池對象的弊端如下:
    1) FixedThreadPool和SingleThreadPool:允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM
    2) CachedThreadPool:允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM

Executors創建線程池,根據返回的對象類型創建線程池有三種:

  • 創建返回ThreadPoolExecutor對象
  • 創建返回ScheduleThreadPoolExecutor對象
  • 創建返回ForkJoinPool對象

這裏只討論創建返回ThreadPoolExecutor對象,其他兩種我也不太懂,就不亂寫了🤐,hh。

ThreadPoolExecutor詳解

在聊Executors 之前,我們必須要先學習一下ThreadPoolExecutor,才知道爲什麼會提到線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,我們就從它的核心參數聊起吧。

ThreadPoolExecutor 的核心參數指的是它在構建時需要傳遞的參數,其構造方法如下所示:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        // maximumPoolSize 必須大於 0,且必須大於 corePoolSize
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize 表示線程池的常駐核心線程數。如果設置爲 0,則表示在沒有任何任務時,銷燬線程池;如果大於 0,即使沒有任務時也會保證線程池的線程數量等於此值。但需要注意,此值如果設置的比較小,則會頻繁的創建和銷燬線程;如果設置的比較大,則會浪費系統資源,所以開發者需要根據自己的實際業務來調整此值(說實話😂,我也不知道咋設纔對,就曉得個概念,hh)。

  • maximumPoolSize 表示線程池最大可以創建的線程數。官方規定此值必須大於 0,也必須大於等於 corePoolSize,此值只有在任務隊列滿時,纔會用到,一個線程池最大承載量等於 maximumPoolSize + workQueue的容量

  • keepAliveTime 表示線程的存活時間,當線程池空閒時並且超過了此時間,多餘的線程就會銷燬,直到線程池中的線程數量銷燬的等於 corePoolSize 爲止,如果 maximumPoolSize 等於 corePoolSize,那麼線程池在空閒的時候也不會銷燬任何線程。

  • unit 表示存活時間的單位,它是配合 keepAliveTime 參數共同使用的。

  • workQueue 表示線程池執行的任務隊列,當線程池的所有線程都在處理任務時,如果來了新任務就會緩存到此任務隊列中排隊等待執行。

  • threadFactory 表示線程的創建工廠,此參數一般用的比較少,我們通常在創建線程池時不指定此參數,它會使用默認的線程創建工廠的方法來創建線程(🤣我也沒設過,都用的默認的)。

  • handler 表示指定線程池的拒絕策略,當線程池的任務已經在緩存隊列 workQueue 中存儲滿了之後,並且不能創建新的線程來執行此任務時,就會用到此拒絕策略,它屬於一種限流保護的機制。

部分參數詳解

比如針對面試題:線程池都有哪幾種工作隊列?或者也叫workQueue 都有哪幾種工作隊列?

  • ArrayBlockingQueue(有界隊列)是一個用數組實現的有界阻塞隊列,按FIFO排序量。
  • LinkedBlockingQueue(可設置容量隊列)基於鏈表結構的阻塞隊列,按FIFO排序任務,容量可以選擇進行設置,不設置的話,將是一個無邊界的阻塞隊列,最大長度爲Integer.MAX_VALUE,吞吐量通常要高於ArrayBlockingQuene;newFixedThreadPool線程池使用了這個隊列
  • DelayQueue(延遲隊列)是一個任務定時週期的延遲執行的隊列。根據指定的執行時間從小到大排序,否則根據插入到隊列的先後排序。newScheduledThreadPool線程池使用了這個隊列。
  • PriorityBlockingQueue(優先級隊列)是具有優先級的無界阻塞隊列;
  • SynchronousQueue(同步隊列)一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQuene,newCachedThreadPool線程池使用了這個隊列

比如針對面試題:線程池的幾種拒絕策略

  • AbortPolicy(直接拋出異常,也是默認的
  • DiscardPolicy(拋棄處理不了的任務,允許任務丟失
  • DiscardOldestPolicy(拋棄隊列中等待的最久的任務
  • CallerRunsPolicy(將處理不了的回退給調用者,也可以理解爲交給線程池調用所在的線程進行處理

如果我們要自定義拒絕策略,那麼只需要新建一個 RejectedExecutionHandler 對象,然後重寫它的 rejectedExecution() 方法即可

線程池是怎麼判斷執行拒絕策略的?我們可以看 execute() ,源碼如下:

    public void execute(Runnable command) {
    	// ...省略其他代碼
    	// 核心線程都在忙且隊列都已爆滿,嘗試新啓動一個線程執行失敗
		(!addWorker(command, false))
			// 執行拒絕策略
            reject(command);
    }

知識擴展 execute() VS submit()
execute() 和 submit() 都是用來執行線程池任務的,它們最主要的區別是,submit() 方法可以接收線程池執行的返回值,而
execute() 不能接收返回值。

使用案例:

public class ThreadPoolWriteDemo {
    public static void main(String[] args) {
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
                2,  // 線程池中的常駐核心線程數
                5,  // 線程池中執行的最大線程數,它包含前者
                2L, // 多餘的空閒線程存活時間
                TimeUnit.SECONDS,   // 時間的單位
                new LinkedBlockingQueue<>(3),   // 等待任務隊列,即被提交單尚未被執行的任務
                Executors.defaultThreadFactory(),   // 生成線程池中工作線程的線程工廠,一般默認即可
                // 直接拋出異常
                new ThreadPoolExecutor.AbortPolicy()    // 拒絕策略
        );
        try {
            for (int i = 0; i < 10; i++) {
                threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t辦理業務");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPoolExecutor1.shutdown();
        }
    }
}

瞭解一下Executors

上面提到線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,其實當我們去看 Executors 的源碼會發現,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底層都是通過ThreadPoolExecutor 實現的。

   /**
    * FixedThreadPool是固定核心線程的線程池,固定核心線程數由用戶傳入
    *
    * corePoolSize => nThreads,核心線程池的數量爲1
    * maximumPoolSize => 等於corePoolSize核心線程數
    * keepAliveTime => 0L
    * unit => 毫秒
    * workQueue => LinkedBlockingQueue
    * 它和SingleThreadExecutor類似,唯一的區別就是核心線程數不同,並且由於使用的是LinkedBlockingQueue,在資源有限的時候容易引起OOM異常
    */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
   /**
    * SingleThreadExecutor是單線程線程池,只有一個核心線程
    * corePoolSize => 1,核心線程池的數量爲1
    * maximumPoolSize => 1,線程池最大數量爲1,即最多只可以創建一個線程,唯一的線程就是核心線程
    * keepAliveTime => 0L
    * unit => 毫秒
    * workQueue => LinkedBlockingQueue
    */    
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
   /**
    * CachedThreadPool是一個根據需要創建新線程的線程池
    *
    * corePoolSize => 0,核心線程池的數量爲0
    * maximumPoolSize => Integer.MAX_VALUE,線程池最大數量爲Integer.MAX_VALUE,可以認爲可以無限創建線程
    * keepAliveTime => 60L
    * unit => 秒
    * workQueue => SynchronousQueue 一個不存儲元素的阻塞隊列
    *
    */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

    // 一池5個受理線程,類似一個銀行有5個受理窗口
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    // 一池1個工作線程,類似一個銀行有1個受理窗口
    ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
    // 一池N個工作線程,類似一個銀行有N個受理窗口
    ExecutorService threadPool2 = Executors.newCachedThreadPool();
    
    try {
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t辦理業務");
            });
            TimeUnit.SECONDS.sleep(1);
        }
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        threadPool.shutdown();
    }

從上述中我們可以看出Executors創建返回ThreadPoolExecutor對象的方法共有三種:

  • Executors#newCachedThreadPool => 創建一個根據需要創建新線程的線程池
  • Executors#newSingleThreadExecutor => 創建單線程的線程池
  • Executors#newFixedThreadPool => 創建固定核心線程的線程池

怎麼終止線程池

	// 關閉,不會接受新的任務,正在執行的任務會繼續執行下去,沒有被執行的則中斷。
    public void shutdown() {}

	// 正在執行的任務則被停止,沒被執行任務的則返回。
    public List<Runnable> shutdownNow() {}
    
	// executor 是否已經關閉了,返回值 true 表示已關閉
    public boolean isShutdown() {}

線程池shutdown與shutdownNow有什麼區別

  • shutdown會把線程池的狀態改爲SHUTDOWN,而shutdownNow把當前線程池狀態改爲STOP
  • shutdown只會中斷所有空閒的線程,而shutdownNow會中斷所有的線程。
  • shutdown返回方法爲空,會將當前任務隊列中的所有任務執行完畢;而shutdownNow把任務隊列中的所有任務都取出來返回。

小結

線程池的使用必須要通過 ThreadPoolExecutor 的方式來創建,這樣纔可以更加明確線程池的運行規則,規避資源耗盡的風險。同時,也介紹了 ThreadPoolExecutor 的七大核心參數,比如核心線程數和最大線程數之間的區別,當線程池的任務隊列沒有可用空間且線程池的線程數量已經達到了最大線程數時,則會執行拒絕策略(4 種),用戶也可以通過重寫 rejectedExecution() 來自定義拒絕策略。

我們下期再見🙈!

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