第十六篇:面試必備的線程池知識-線程池的使用

前言

學習完了阻塞隊列之後,接下來,我們學習下面試必考的知識點–線程池。用過JAVA的同學一定聽過線程池的大名。下面我們就來看看這個大名鼎鼎的傢伙。

爲啥要用線程池呢?

第一個問題來了,爲啥要使用線程池呢?直接new一個線程它不香麼?就像這樣

new Thread(new Runnable() {
            public void run() {
                longTest.countTest();
            }
        }).start()

簡單又快捷,其中run方法的作用是用來執行一個任務單元(也就是一段代碼)。start方法的作用是用來創建一個新線程,同時設置好這個線程的上下文,比如這個線程的棧,線程的狀態等一系列的信息。
這些信息處理好之後這個線程纔可以被調度,一旦調度,就會執行run()方法。
但是在實際項目中是禁止這樣做了,在阿里出的JAVA開發手冊中就明確說了原因:

所以,直接new一個線程不香,原因主要在於創建大量相同的線程會大量的消耗系統內存,甚至會導致系統內存耗盡;同時,大量的線程會競爭CPU的調度,導致CPU過度切換。
在這裏插入圖片描述
接下來我們就看看香香的線程池的優點。

線程池的優點

  1. 減少在創建和銷燬線程上所花費的時間和系統資源的開銷
  2. 提高響應速度,當任務到達之後,任務可以不需要等到線程創建就能被立即執行
  3. 提高線程的可管理性,線程是稀缺資源,如果無限制的的創建,不僅會消耗系統資源,還會降低系統性能,使用線程池可以進行統一分配,調優和監控。

線程池長啥樣呢?

前面說了一堆線程池的好處,下面我們來看看線程池的結構。

創建線程的幾種方式

  1. 創建一個緩存線程池
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

由於SynchronousQueue是一個無界的隊列,當任務過多時,大量的任務堆積到隊列裏可能會發生OOM異常(Java內存溢出異常),同時線程池的最大線程數也沒有限制,創建大量線程缺點前面也說了,綜上所述不推薦使用這種方式創建線程池
2. 創建固定容量的線程池

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

線程池的容量固定爲傳入值nThreads,任務的阻塞隊列用的是LinkedBlockingQueue,沒有指定隊列的容量,所以隊列的最大容量可達Integer.MAX_VALUE。當有大量請求時,可能造成任務的大量堆積,發生OOM異常(Java內存溢出異常)。綜上所說實際項目中不推薦使用這種方式創建線程池
3. 創建單個線程的線程池

 public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

這個線程池中線程的容量只有1個,可用於一些特殊的場景下。
從上,我們知道Executors類中各種創建線程池的方法,其實內部都調用的是ThreadPoolExecutor的構造器,那麼我們就來看看ThreadPoolExecutor類。
在這裏插入圖片描述

ThreadPoolExecutor類

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor類的構造方法參數比較多,下面分別介紹下各個參數:

  1. corePoolSize: 核心線程數的大小,就是線程池中始終保留的存活線程的數量,這個就相當於項目組的常駐組員。
  2. maximumPoolSize: 線程池中允許最大的線程數,當任務比較多時,可以適當增加到線程,只要總的線程數量不超過最大線程數。就相當於項目組常駐組員數+外援組員數<=最大的組員數
  3. keepAliveTime :空閒線程允許的最大存活時間,當一個線程空閒超過這段時間,就會被回收
  4. unit :存活時間的時間單位
  5. workQueue :保存任務的阻塞隊列,所有需要線程執行的任務都會保存到這個隊列中,當線程被CPU調度之後就會從隊列中取出一個任務來執行。
  6. threadFactory: 線程工廠用來創建線程,通過這個參數可以自定義如何創建線程,例如:你可以給線程指定一個有意義的名字
  7. handler: 拒絕策略,針對當隊列滿了是 新來任務的處理方式,如果線程池中所有的線程都在忙碌,並且工作隊列也滿了(前提是工作隊列是有界隊列),那麼此時提交任務,線程池就會拒絕接受,至於拒絕的策略,可以通過handler這個參數來指定,ThreadPoolExector已經提供了以下4中策略。
    • CallerRunsPolicy: 提交任務的線程自己去執行該任務
    • AbortPolicy: 默認的拒絕策略,會拋出 RejectedExecutionException。
    • DiscardPolicy: 直接丟棄任務,沒有任何異常拋出
    • DiscardOldestPolicy: 丟棄最老的任務,其實就是把最早進入工作隊列的任務丟棄,然後把新任務加入到工作隊列。

創建線程池的正確姿勢是啥呢?

      ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("測試-%d").build();
        ExecutorService executorService = new ThreadPoolExecutor(10, 15, 1, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

就像如上所示,創建的線程池,指定了核心線程數以及最大線程數,同時,指定了任務隊列的容量,這個很重要,最後,給線程指定了一個有意義的名字。

線程池的執行

 se.execute(new Job);

這樣的方式提交一個任務到線程池,所以核心的邏輯就是execute()函數。

源碼解析

    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.
         */
		//1.獲取線程池的狀態
	   int c = ctl.get();
	   //2.當前線程數量小於corePoolSize時創建一個新的線程運行
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
		//3.如果當前線程處於運行狀態,並且寫入阻塞隊列成功
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
			//4.雙重檢查,再次獲取線程狀態,如果線程狀態變了(非運行狀態)
			//就需要從阻塞隊列移除任務,並嘗試判斷線程是否全部執行完畢,同時執行拒絕策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
				//5.如果當前線程池爲空就新創建一個線程並執行
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
		//6.如果在第三步的判斷爲非運行狀態,嘗試新建線程,如果失敗則執行拒絕策略。
        else if (!addWorker(command, false))
            reject(command);
    }

方法說明:該方法核心的邏輯主要是如下三步:

  1. 如果當前在可運行的線程數量小於corePoolSize時,則創建一個新線程運行。
  2. 如果任務成功的加入到隊列中,則進行雙重檢查,再次獲取線程狀態,因爲線程的狀態可能變成了非運行狀態
  3. 如果是非運行狀態,則嘗試創建一個線程,如果失敗則執行拒絕策略。

線程池的配置

按照經驗,我們首先可以分析線程池需要執行的任務是那種類型:
對於 IO 密集型任務:由於線程並不是一直在運行,所以可以儘可能的多配置線程,比如CPU個數*2
對於CPU密集型任務(大量複雜的運算)應當分配較少的線程,比如CPU個數相當的大小。

如何優雅的關閉線程池?

關閉線程池無非就是兩種方法shutdown()/shutdownNow()
這兩個方法有着重要的區別:

  1. shutdown()執行後停止接受新任務,會把隊列的任務執行完畢。
  2. shutdownNow()也是停止接受新任務,但會中斷所有的任務,將線程池的狀態改成stop。 shutdownNow()更加的簡單粗暴,可以根據實際場景來選用不同的方法。

如何SpringBoot中整合線程池?

首先是利用好SpringBoot的自動裝配功能,配置好線程池的一些基本參數。

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {

    /*
    * 線程池名前綴
    */
    private static final String threadNamePrefix = "Api-Async-";


    /**
     * bean的名稱, 默認爲首字母小寫的方法名
     * @return
     */
    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        /**
     * 默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,
     * 當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
     * 當隊列滿了,就繼續創建線程,當線程數量大於等於maxPoolSize後,開始使用拒絕策略拒絕
     */
    /*
    * 核心線程數(默認線程數)
    */
		executor.setCorePoolSize(corePoolSize);
		//最大線程數
        executor.setMaxPoolSize(maxPoolSize);
		//緩衝隊列數
        executor.setQueueCapacity(queueCapacity);
		//允許線程空閒時間(單位是秒)
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        //用來設置線程池關閉時候等待所有任務都完成再繼續銷燬其他的Bean
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //線程池對拒絕任務的處理策略,CallerRunsPolicy:由調用線程(提交任務的線程)處理該任務
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化
        executor.initialize();
        return executor;
    }
}

配置好線程池的基本參數時候,我們就可以使用線程池了, 只要在一個限定域爲public的方法頭部加上@Async註解即可。

  @Async
    public void createOrder() {
          System.out.println("執行任務"); 
    }

總結

本文主要介紹了介紹了線程池的優點,其主要就是減少創建創建和銷燬線程所花費的時間和系統資源的開銷,然後,介紹了Executors中幾種創建線程池的方式,不過不推薦使用,接着介紹了正確創建線程池的姿勢,着重介紹了ThreadPoolExecutor類的構造器和execute()方法。最後提到了在SpringBoot中使用線程池。

參考

線程和線程池
如何優雅的使用和理解線程池
關於Java多線程及線程池的使用看這篇就夠了
https://time.geekbang.org/column/article/90771

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