線程池原理分析(ThreadPoolExecutor源碼分析)

線程池的優點

  1. 線程是稀缺資源,使用線程池可以減少創建銷燬線程的次數,每個工作線程都可以重複使用。
  2. 可以根據系統的承受能力,調整線程池中工作線程的數量,防止因爲消耗過多內存導致服務器崩潰。

線程池的創建

創建時,有多個構造方法,參數個數不同,最終都調用下面的構造方法進行創建。

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

參數含義:

  • corePoolSize:線程池核心線程數量
  • maximumPoolSize:線程池最大線程數量
  • keepAliveTime:當活躍線程數大於核心線程數時,空閒的多餘線程的最大存活時間
  • unit:存活時間的單位
  • workQueue:存放任務的隊列
    常用的workQueue類型:
  1. SynchronousQueue:這個隊列接收到任務的時候,會直接提交給線程處理,而不保留它,如果所有線程都在工作怎麼辦?那就新建一個線程來處理這個任務!所以爲了保證不出現<線程數達到了maximumPoolSize而不能新建線程>的錯誤,使用這個類型隊列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大。
  2. LinkedBlockingQueue:這個隊列接收到任務的時候,如果當前線程小於核心線程數,則新建線程(核心線程)處理任務,如果當前線程數等於核心線程數,則進入隊列等待調度。由於這個隊列默認設置容量爲Integer.MAX_VALUE,即超過核心線程數的任務都將加載到隊列中,這個隊列會導致maximumPoolSize參數設置失效,因爲線程池中線程數永遠等於corePoolSize。
  3. ArrayBlockingQueue:可以限定隊列的長度,接收到任務的時候,如果沒有達到corePoolSize值,則新建核心線程執行任務,如果達到了,則入隊等待,如果隊列已滿,則新建非核心線程執行任務,如果達到了總線程數,並且隊列也滿了,則報錯。
  4. DelayQueue:隊列內元素必須實現Delayed接口,這就意味着你穿進去的任務必須先實現Delayed接口。這個隊列接收到任務時,首先入隊,達到指定的延時時間才執行任務,相當於延時隊列。
  • threadFactory:創建線程的方式。
  • handler:超出線程範圍和隊列容量的任務的處理程序
    任務拒絕策略:
    1、ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
    2、ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
    3、ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
    4、ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

常見線程池

CachedThreadPool()

可緩存線程池,線程數無限制(Integer.MAX_VALUE),有空閒線程則複用空閒線程,若無則新建線程

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

FixedThreadPool()

定長線程池,可控制線程最大併發數,超過線程數的任務在隊列中等待。

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

ScheduledThreadPool()

有時間延時的線程池,這個線程池間接的利用ThreadPoolExecutor創建。支持定時及週期性任務執行。任務隊列使用的是延時隊列,可以規定時間延遲。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

SingleThreadExecutor()

單線程線程池,核心線程和最大線程數都爲1,所有任務按指定順序執行,遵循隊列的入隊出隊規則。

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

SingleThreadScheduledExecutor()

單線程可延時線程池,該線程池結合了ScheduledThreadPool和SingleThreadExecutor兩個線程池的內容。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

線程池的實現原理

提交一個任務到線程池中,線程池的處理流程如下:

1、判斷線程池中的核心線程是否都在執行任務,如果沒有則創建一個新的工作線程來執行任務。如果核心線程數已經達到創建限制,並且核心線程都在執行任務,則進行下個流程。

2、線程池判斷任務隊列是否已滿,如果任務隊列未滿,則將任務提交存儲到任務隊列中。如果任務隊列滿了,則進入下個流程。

3、判斷線程池中的線程是否都處於工作狀態,如果沒有,則創建一個工作線程(非核心線程)來執行任務,如果這時,線程都在工作且線程數達到線程池最大線程數量,則交給飽和策略來處理這個任務(handler)。

在這裏插入圖片描述

源碼分析

下面寫一個簡單例程進行源碼分析的入口,測試demo如下:

public class ThreadPoolTest {
    public static void main(String[] args) {
        LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);
        for (int i = 0; i < 16; i++) {
            poolExecutor.execute(()->{
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println("線程池中活躍的線程數:"+ poolExecutor.getPoolSize());
            if (queue.size() > 0){
                System.out.println("-------------------任務隊列阻塞的線程數"+queue.size());
            }
        }
        poolExecutor.shutdown();
    }
}
  • 首先查看線程池ThreadPoolExecutor類的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.
         */
        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);
            //判斷可用線程數是否等於0,是,開啓工作線程執行任務
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
    //嘗試開啓工作線程,如果失敗,則提交給飽和策略
        else if (!addWorker(command, false))
            reject(command);
    }
  • 接下來,我們查看addWorker方法,創建線程的方法。該方法有兩個參數,而第二個core參數,表示的創建的是核心線程還是非核心線程,傳參爲boolean型。
    1. 如下圖第一個框內,是進行判斷,判斷線程池狀態大於等於shutdown(即狀態不是運行中),並且再滿足後面的條件,則不處理直接返回。
    2. 第二個框中,是更新線程池線程數量,根據傳入的core 參數,判斷是否爲核心線程,如果爲true,且線程數小於corePoolSize,則跳出循環,創建核心線程。

在這裏插入圖片描述

​ 3.接下來的代碼如下圖,第一個框中,將任務封裝到Worker類中,並獲得線程池主鎖,保證線程安全。在這裏,線程池中的線程是通過Worker類來實現的。

​ 4.第二個框中,再次校驗線程池狀態和剛創建的線程狀態,接下來將線程加入線程池中(workes)。

​ 5.第三個框中,啓動剛創建的線程。

在這裏插入圖片描述

  • 下面我們查看Worker類的run方法,run方法中調用了runWorker方法,源碼如下圖。
    1. 第一個框中,當該線程是剛創建第一次執行任務,或者從任務隊列中獲取到任務,首先判斷線程池狀態,如果線程池不是停止狀態,清除當前線程的中斷標誌後再次判斷線程池狀態;如果前者判斷爲true,接下來再次判斷當前線程是否爲中斷,再結合前者條件,true:將當前線程中斷,false:執行當前線程。
    2. 第二個框中,執行任務開始前操作鉤子。
    3. 第三個框中,執行任務。
    4. finally塊中,執行執行任務後鉤子。

在這裏插入圖片描述
在上述簡單demo中,運行後會出現如下錯誤,因爲我們設置的LinkedBlockingQueue隊列的容量是5,核心線程爲5,最大線程數爲10,for循環將開啓總共16個線程執行16次任務。當線程池中線程數達到最大線程數10後,開始往隊列中存儲任務,存儲5個,隊滿後,線程數和隊列容量都達到了臨界點,所以執行飽和策略,由於未定義,所以執行默認的RejectedExecutionException異常。

線程池中活躍的線程數:7
-------------------任務隊列阻塞的線程數5
線程池中活躍的線程數:8
-------------------任務隊列阻塞的線程數5
線程池中活躍的線程數:9
-------------------任務隊列阻塞的線程數5
線程池中活躍的線程數:10
-------------------任務隊列阻塞的線程數5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at threadPools.ThreadPoolTest.main(ThreadPoolTest.java:19)

接下來,我們定義一個丟棄策略,在創建線程池時將該參數傳入,再次執行將不會出現上述異常,因爲丟棄策略是將超出限制的線程丟棄不執行。除此之外,還可以自定義其他飽和策略來處理當線程池達到飽和狀態的情況。

public class ThreadPoolTest {
    public static void main(String[] args) {
        LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
        //丟棄策略,多餘的丟棄,不報錯
        ThreadPoolExecutor.DiscardPolicy handler = new ThreadPoolExecutor.DiscardPolicy();
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue,handler);
        for (int i = 0; i < 16; i++) {
            poolExecutor.execute(()->{
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println("線程池中活躍的線程數:"+ poolExecutor.getPoolSize());
            if (queue.size() > 0){
                System.out.println("-------------------任務隊列阻塞的線程數"+queue.size());
            }
        }
        poolExecutor.shutdown();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章