任務執行服務的主要實現機制:線程池

定義

線程池,就是一個線程的池子,裏面有若干線程,它們的目的就是執行提交給線程池的任務,執行完一個任務後不會退出,而是繼續等待或執行新任務執行完一個任務後不會退出,而是繼續等待或執行新任務。

線程池主要是由兩個概念組成:一個是任務隊列;另一個是工作者線程。工作者線程主體就是一個循環,循環從隊列中接受任務並執行,任務隊列保存待執行的任務。

線程池的優點是顯而易見的:

(1) 它可以重用線程,避免線程創建的開銷。

(2)任務過多時,通過炮隊避免創建過多的線程,減少系統資源消耗和競爭,確保任務有序完成。

理解線程池的一些參數

主要的構造方法

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

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

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

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;
    }

參數corePoolSize、maximumPoolSize、keepAliveTime、unit用於控制線程池中線程的個數,workQueue 表示任務隊列,threadFactor 用於對創建的線程進行一些配置,handler 表示任務拒絕策略。

線程池大小

maximumPoolSize 表示線程池的最多線程數,線程的個數會動態變化,但這是最大值,不管有多少任務,都不會創建比這個值最大的線程個數。

corePoolSize 表示線程池中的核心線程個數,不過,並不是一開始就創建這麼多線程,剛創建一個線程池後,實際上並不會創建任何線程。

一般情況下,有新任務到來的時候,如果當前線程個數小於corePoolSize,就會創建一個新線程來執行該任務,需要說明的是,即使其他線程現在也是空閒的,也會創建新的線程。不過,如果線程個數大於等於corePoolsize,那就不會立即創建新線程了,它會先嚐試排隊,需要強調的是,它是“嘗試排隊”,而不是“阻塞等待” 入隊,如果隊列滿了或其他原因不能立即入隊,它就不會排隊,而是檢查線程的個數是否達到了maximumPoolSize ,如果沒有,就會繼續創建線程,直到線程數達到maximumPoolSize.

keepAliveTime 的目的是爲了釋放多餘的線程資源,它表示,當線程池中的線程個數大於corePoolSize時,額外空閒線程的存活時間。也就是說,一個非核心線程,在空閒等待新任務時,會有一個最長等待時間,即keepAliveTime,如果到了時間還是沒有新任務,就會被終止。如果該值爲0,則表示所有線程都不會超時終止。

隊列

ThreadPoolExecutor 要求的隊列類型是阻塞隊列BlockingQueue。

比如:

LinkedBlockingQueue:基於鏈表的阻塞隊列,可以指定最大長度,但默認是無界的。

ArrayBlockingQueue:基於數組的有界阻塞隊列。

PriorityBlockingQueue:基於堆的無界阻塞優先級隊列。

SynchronousQueue:沒有實際存儲空間的同步阻塞隊列。

如果用的是無界隊列,需要強調的是,線程個數最多隻能達到corePoolSize,到達corePoolSize後,新的任務總會排隊,參數maximumPoolSize 也就沒有意義了。

對於SynchronousQueue,我們知道,它沒有實際存儲元素的空間,當嘗試排隊時,只有正好有空閒線程在等待接受任務時,纔會入隊成功,否則,總是會創建新線程,直到達到maximumPoolSize。

任務拒絕策略

如果隊列有界,且maximumPoolSize 有限,則當隊列排滿,線程個數也達到了了maximumPoolSize,這時,新任務來了,如何處理呢?此時,會觸發線程池的任務拒絕策略。

提交任務的方法(execute/submit/invokeAll)會拋出異常,類型爲RejectedException。

ThreadPoolExecutor實現了四種處理方式:

(1)ThreadPoolExecutor.AbortPolicy:這就是默認的方式沒跑出異常。

(2)ThreadPoolExecutor.DiscardPolicy:靜默處理,忽略新任務,不拋出異常,也不執行。

(3)ThreadPoolExecutor.DiscardOldestPolicy: 將等待時間最長的任務扔掉,然後自己排隊。

(4)ThreadPoolExecutor.CallerRunsPolicy:在任務提交者線程中執行任務,而不是交給線程池中線程執行。

它們都是ThreadPoolExecutor 的public靜態內部類,都實現了RejectedExecutionHandler接口,這個接口的定義爲:

public interface RejectedExecutionHandler {

    /**
     * Method that may be invoked by a {@link ThreadPoolExecutor} when
     * {@link ThreadPoolExecutor#execute execute} cannot accept a
     * task.  This may occur when no more threads or queue slots are
     * available because their bounds would be exceeded, or upon
     * shutdown of the Executor.
     *
     * <p>In the absence of other alternatives, the method may throw
     * an unchecked {@link RejectedExecutionException}, which will be
     * propagated to the caller of {@code execute}.
     *
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

默認的RejectedExecutionHandler是一個AbortPolicy 實例。如下:

private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();
 /**
     * A handler for rejected tasks that throws a
     * {@code RejectedExecutionException}.
     */
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

拒絕策略只有在隊列有界,且maximumPoolSize 有限的情況下才會觸發。如果隊列無界,服務不了的任務總會排隊,但這不一定是期望的結果,因爲請求處理隊列可能會消耗非常大的內存,甚至引發內存不夠的異常,如果隊列有界,但maximumPoolSize 無限,可能會創建過多的線程,佔滿CPU和內存,使得任何任務都難以完成。所以,在任務量非常大的場景中,讓拒絕策略有機會執行是保證系統穩定運行很重要的方面。

線程工廠

線程池還可以接受一個參數,ThreadFactory。它還是一個接口,定義爲:

public interface ThreadFactory {

    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

根據Runnable 創建一個Thread。 ThreadPoolExecutor的默認實現是Executors類中的靜態內部類DefaultThreadFactory,主要就是創建一個線程,給線程設置一個名稱,設置daemon屬性爲false,設置線程優先級爲標準默認優先級,線程名稱的格式爲:pool-<線程池編號>-thread-<線程編號>。如果需要自定義一些線程的屬性,比如名稱,可以實現自定義的ThreadFactory。

static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

核心線程的特殊配置:

線程個數小於等於corePoolSize時,我們稱這些線程爲核心線程,默認情況下:

(1) 核心線程不會預先創建,只有當有任務時,纔會創建。

(2)核心線程不會因爲空閒而被終止,keepAliveTime 參數不適用於它。

//預先創建所有線程
public int prestartAllCoreThreads() 

// 創建一個核心線程,如果所有核心線程都已創建,則返回false
public boolean prestartCoreThread() 

// 如果參數爲true,則keepAliveTime 參數也適用於核心線程
public void allowCoreThreadTimeOut(boolean value)

工廠類Executors

提供了一些靜態的工廠方法,可以方便地預創建一些線程池

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

只使用一個線程,使用無界隊列LinkedBlockingQueue,線程創建後不會超時終止,該線程順序執行所有任務。該線程池適用於需要確保所有任務被順序執行的場合。

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

使用固定數目的n個線程,使用無界隊列LinkedBlockingQueue,線程創建後,不會超時終止。和newSingleThreadExecutor一樣,由於是無界隊列,如果排隊任務過多,可能會消耗過多的內存。

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

它的corePoolSize爲 0 ,maximumPoolSize 爲Integer.Max_Value, keepAliveTime 是60秒,隊列爲SynchronousQueue。它的含義是:當新任務到來時,如果正好有空閒線程在等待任務,則其中一個空閒線程接受該任務,否則就總是創建一個新線程,創建的總線程個數不受限制,對任一空閒線程,如果60秒內沒有新任務,就終止。

實際中,應該使用newFixedThreadPool 還是newCachedThreadPool呢?

在系統負載很高的情況下,newFixedThreadPool 可以通過隊列對新任務排隊,保證有足夠的資源處理實際的任務,而newCachedThreadPool 會爲每個任務創建一個線程,導致創建過多的線程競爭CPU和內存資源,使得任何實際任務都難以完成,這時,newFixedThreadPool 更爲適用。

不過,如果系統負載不是太高,單個任務的執行時間也比較短,newCachedThreadPool 的效率可能更高,因爲任務可以不經排隊,直接交給某一個空線程。

在系統負載可能極高的情況下,兩者都不是好的選擇,newFixedThreadPool的問題是隊列過長,而newCachedThreadPool的問題是線程過多,這時,應根據具體情況自定義ThreadPoolExecutor,傳遞合適的參數。

線程池的死鎖

任務之間有依賴,這種情況可能會出現死鎖。比如任務A,在它的執行過程中,它給同樣的任務執行服務提交了一個任務B,但需要等待任務B結束。

如果任務A是提交了一個單線程池,一定會出現死鎖,A在等待B的結果,而B在隊列中等待調度。如果是提交給了一個限定線程個數的線程池,也能因線程數限制出現死鎖。

怎麼解決這種問題呢?可以使用newCachedThreadPool創建線程池,讓線程數不受限制。另一個解決方法是使用SynchronousQueue,它可以避免死鎖,怎麼做到的呢?對於普通隊列,入隊只是把任務放到了隊列中,而對於SynchronousQueue來說,入隊成功就意味着已有線程接受處理,如果入隊失敗,可以創建更多線程直到maximumPoolSize,如果達到了maximumPoolSize,會觸發拒絕機制,不管怎麼樣,都不會死鎖。

參考文章

java編程的邏輯基礎(馬俊昌)

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