多線程:線程池的原理和使用解析

目錄

1,線程池相關的類接口(類)及其關係

2,線程池的實現原理(即任務的處理流程)

3,線程池的幾種創建方式

4,使用線程池提交任務

5,關閉線程池


1,線程池相關的類接口(類)及其關係

    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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

    1,核心線程數量:有的地方也叫線程池基本大小,是保留在線程中的線程數量,即使他們是空閒的,當一個任務提交到線程池中時,只要需要執行的線程數小於核心線程數機會創建線程。如果調用prestartAllCoreThreads()方法線程會提前創建並啓動所有基本的線程。

    2,最大線程數量:就是允許創建的最大線程數量。

    3,keepAliveTime:空閒線程存活的時間,這個空閒線程指的是外包線程(核心線程之外的線程)。所以如果任務很多而且每個執行的時間較短,可以調大時間,這樣避免再次創建線程,提高了線程利用率。

    4,任務隊列:用於保存等待執行的任務的阻塞隊列,有一下幾種選擇。

        ArrayBolckingQueue:基於數組有界,fifo

        LinkedBlockingQueue:基於鏈表,fifo,吞吐量比上一個高,靜態工廠方法Executors.newFixedThreadPool使用它

        SychronousQueue:不存儲元素的阻塞隊列,每個插入必須等另一個移除,newCachedThreadPool使用它

        PriorityBlockingQueue:一個具有優先級的無限阻塞隊列

    5, ThreadFactory:用於常見線程的工廠,可以設置共有意義的線程名

    6,飽和策略:隊列和線程池都滿了如何處理

2,線程池的實現原理(即任務的處理流程)

    ThreadPoolExecutor的execute(Runnable command)方法如下:

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) {    (1)
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { (2)
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))    (3)
            reject(command);
    }

   線程池處理流程

    (1),判斷運行線程是否小於核心線程數,如果是,則創建新線程來執行任務(此步驟需獲取全局鎖),直到核心線程池滿了爲止,如果滿了,進行下一流程。

    (2),判斷工作隊列是否已滿,如果沒有,將新提交的任務存儲在工作隊列BlockingQueue裏。如果工作隊列滿了,進入下一個流程。

    (3),判斷最大線程數maximumPoolSize是否已滿,如果沒有,創建一個新的工作線程執行任務(此步驟需獲取全局鎖)。如果滿了,交給飽和策略處理。

    工作線程所在的優先級順序是:核心線程池 > 工作隊列 > 最大線程池。

下面以圖片的形式展示線程池的處理流程以及ThreadPoolExecutor的執行示意圖

    ThreadPoolExecutor採用上述思路,爲的是在執行esecute()方法時儘可能少的獲取全局鎖,在ThreadPoolExecutor完成預熱之後(即當前運行的線程數大於corePoolSize),幾乎所有的execute()方法都調用步驟2,而步驟2不需要獲取全局鎖。

    工作線程:線程池創建線程時,會將線程封裝成工作現成Worker,Worker在執行完任務後,還會獲取工作隊列裏的任務來執行。Worker類的run()方法。

    (1),execute()裏的addWorker()方法創建一個線程,會讓這個線程執行當前任務。

    (2),完成1中的任務後,會從BlockingQueue中獲取任務來執行。

3,線程池的幾種創建方式

    線程池中使用Executors中的一系列靜態方法例如:newFixedThreadPool(...),newSingleThreadPool(...),newCachedThreadPool(...)來創建線程,而這些方法在實現時都是依賴ThreadPoolExecutor的構造方法,只是構造方法裏的參數不同而已。

    1,newFixedThreadPool 方法

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

    由代碼可知,它的核心線程數和最大線程數相等,所以在完成預熱後,他的線程個數是固定的,有了新的任務就塞進LinkedBlockingQueue裏,等待工作線程空閒時執行任務。由於核心和最大線程數相等,所以不會有多餘的線程,keepAliveTime這個參數也就沒有什麼意義了。

    這個方法的應用場景就是爲了限制線程使用的資源,使用於負載比較重的服務器。缺點就是因爲LinkedBlockingQueue的容量是Integer.MAX_VALUE,所以無法拒絕任務,也就是飽和策略RejectedExecutorHandler根本不會用到。

2,newSingleThreadExecutor 方法(注意他的名後綴是Executor而不是SingleThreadPool

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

    這個方法和之前的newFixedThreadPool類似,只不過是把固定的線程數換成只有一個線程,他的使用場景就是想要任務按照順序執行;任意時間點,不會有多個線程是活動的場景。因爲使用了無界的鏈表隊列,所以也無法拒絕任務。

    

3,newCachedThreadPool 方法(需要理解這個Cached的含義,爲什麼這麼起名?)

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

    這個方法和之前的兩個區別很大,沒有核心線程,全部都是外包線程,所以keepAliveTime可以對線程池的所有線程起作用,

默認的等待時間是60秒,但是他的隊列是沒有容量的(或者說是容量爲1),這樣如果主線程提交任務的速度大於線程中處理任務的速度,那麼就會沒有空閒線程,就會一直創建線程,造成CPU和內存大量消耗。

    這種線程池一般適用於很多的短期異步小程序(爲什麼是異步?因爲它的隊列沒有容量,進來一個任務就可以立即執行),而且最好是不怕佔用資源(也就是在負載較輕的服務器用)。

4,使用線程池提交任務

    可以使用execute和submit方法向線程池提交任務,execute()方法不需要返回值,只需要提交一個Runnable類的實例就可以。而submit方法是在AbstractExecutorService就實現的方法,用於提交需要返回值的任務,線程池會返回一個Future類型的對象,通過這個對象來判斷任務是否執行成功,可以通過Future的get()方法獲取返回值,get方法會阻塞當前線程直到任務返回,當然也可以設置阻塞時間。

public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 處理中斷異常
} catch (ExecutionException e) {
// 處理無法執行任務異常
} finally {
// 關閉線程池
executor.shutdown();
}

5,關閉線程池

    可以通過shutdown或shutdownNow來關閉線程池,他的原理是遍歷線程池中的工作線程,然後逐個調用interrupt方法來中斷線程。

    shutdown只是將線程池的狀態設置爲SHUTWDOWN狀態,正在執行的任務會繼續執行下去,沒有被執行的則中斷。而shutdownNow則是將線程池的狀態設置爲STOP,正在執行的任務則被停止,沒被執行任務的則返回。shutdownNow可說比shutdown關的更徹底。

    只要調用了任意一個,isShutdown方法都會返回true,當所有的任務都關閉後,表示線程線程池關閉成功,這時isTerminaed方法會返回true。

在ThreadPoolExecutor中定義了關於線程狀態的幾個變量如下:

   // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

       1.當創建線程池後,初始時,線程池處於RUNNING狀態,此時線程池中的任務爲0;

  2.如果調用了shutdown()方法,則線程池處於SHUTDOWN狀態,此時線程池不能夠接受新的任務,它會等待所有任務執行完畢;

  3.如果調用了shutdownNow()方法,則線程池處於STOP狀態,此時線程池不能接受新的任務,並且會去嘗試終止正在執行的任務;

        4.當所有的任務已終止,ctl記錄的”任務數量”爲0,線程池會變爲TIDYING狀態。接着會執行terminated()函數。

  5.線程池處在TIDYING狀態時,執行完terminated()之後,就會由 TIDYING -> TERMINATED,線程池被設置爲TERMINATED狀態。

 

參考:《Java併發編程的藝術》

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