Java 執行緒池(ThreadPoolExecutor)原理分析與使用

孫_悟_空發表於2017-06-01

在我們的開發中“池”的概念並不罕見,有資料庫連線池、執行緒池、物件池、常量池等等。下面我們主要針對執行緒池來一步一步揭開執行緒池的面紗。

使用執行緒池的好處

1、降低資源消耗

可以重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。

2、提高響應速度

當任務到達時,任務可以不需要等到執行緒建立就能立即執行。

3、提高執行緒的可管理性

執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控

執行緒池的工作原理

首先我們看下當一個新的任務提交到執行緒池之後,執行緒池是如何處理的

1、執行緒池判斷核心執行緒池裡的執行緒是否都在執行任務。如果不是,則建立一個新的工作執行緒來執行任務。如果核心執行緒池裡的執行緒都在執行任務,則執行第二步。

2、執行緒池判斷工作佇列是否已經滿。如果工作佇列沒有滿,則將新提交的任務儲存在這個工作佇列裡進行等待。如果工作佇列滿了,則執行第三步

3、執行緒池判斷執行緒池的執行緒是否都處於工作狀態。如果沒有,則建立一個新的工作執行緒來執行任務。如果已經滿了,則交給飽和策略來處理這個任務

執行緒池飽和策略

這裡提到了執行緒池的飽和策略,那我們就簡單介紹下有哪些飽和策略:

AbortPolicy

為Java執行緒池預設的阻塞策略,不執行此任務,而且直接丟擲一個執行時異常,切記ThreadPoolExecutor.execute需要try catch,否則程式會直接退出。

DiscardPolicy

直接拋棄,任務不執行,空方法

DiscardOldestPolicy

從佇列裡面拋棄head的一個任務,並再次execute 此task。

CallerRunsPolicy

在呼叫execute的執行緒裡面執行此command,會阻塞入口

使用者自定義拒絕策略(最常用)

實現RejectedExecutionHandler,並自己定義策略模式

下我們以ThreadPoolExecutor為例展示下執行緒池的工作流程圖

這裡寫圖片描述

這裡寫圖片描述

1、如果當前執行的執行緒少於corePoolSize,則建立新執行緒來執行任務(注意,執行這一步驟需要獲取全域性鎖)。

2、如果執行的執行緒等於或多於corePoolSize,則將任務加入BlockingQueue。

3、如果無法將任務加入BlockingQueue(佇列已滿),則在非corePool中建立新的執行緒來處理任務(注意,執行這一步驟需要獲取全域性鎖)。

4、如果建立新執行緒將使當前執行的執行緒超出maximumPoolSize,任務將被拒絕,並呼叫RejectedExecutionHandler.rejectedExecution()方法。

ThreadPoolExecutor採取上述步驟的總體設計思路,是為了在執行execute()方法時,儘可能地避免獲取全域性鎖(那將會是一個嚴重的可伸縮瓶頸)。在ThreadPoolExecutor完成預熱之後(當前執行的執行緒數大於等於corePoolSize),幾乎所有的execute()方法呼叫都是執行步驟2,而步驟2不需要獲取全域性鎖。

關鍵方法原始碼分析

我們看看核心方法新增到執行緒池方法execute的原始碼如下:

     //
     //Executes the given task sometime in the future.  The task
     //may execute in a new thread or in an existing pooled thread.
     //
     // If the task cannot be submitted for execution, either because this
     // executor has been shutdown or because its capacity has been reached,
     // the task is handled by the current {@code RejectedExecutionHandler}.
     //
     // @param command the task to execute
     // @throws RejectedExecutionException at discretion of
     //         {@code RejectedExecutionHandler}, if the task
     //         cannot be accepted for execution
     // @throws NullPointerException if {@code command} is null
     //
    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.
         // 翻譯如下:
         // 判斷當前的執行緒數是否小於corePoolSize如果是,使用入參任務通過addWord方法建立一個新的執行緒,
         // 如果能完成新執行緒建立exexute方法結束,成功提交任務
         // 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.
         // 翻譯如下:
         // 在第一步沒有完成任務提交;狀態為執行並且能否成功加入任務到工作佇列後,再進行一次check,如果狀態
         // 在任務加入佇列後變為了非執行(有可能是在執行到這裡執行緒池shutdown了),非執行狀態下當然是需要
         // reject;然後再判斷當前執行緒數是否為0(有可能這個時候執行緒數變為了0),如是,新增一個執行緒;
         // 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.
         // 翻譯如下:
         // 如果不能加入任務到工作佇列,將嘗試使用任務新增一個執行緒,如果失敗,則是執行緒池已經shutdown或者執行緒池
         // 已經達到飽和狀態,所以reject這個他任務
         //
        int c = ctl.get();
        // 工作執行緒數小於核心執行緒數
        if (workerCountOf(c) < corePoolSize) {
            // 直接啟動新執行緒,true表示會再次檢查workerCount是否小於corePoolSize
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 如果工作執行緒數大於等於核心執行緒數
        // 執行緒的的狀態未RUNNING並且佇列notfull
        if (isRunning(c) && workQueue.offer(command)) {
            // 再次檢查執行緒的執行狀態,如果不是RUNNING直接從佇列中移除
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                // 移除成功,拒絕該非執行的任務
                reject(command);
            else if (workerCountOf(recheck) == 0)
                // 防止了SHUTDOWN狀態下沒有活動執行緒了,但是佇列裡還有任務沒執行這種特殊情況。
                // 新增一個null任務是因為SHUTDOWN狀態下,執行緒池不再接受新任務
                addWorker(null, false);
        }
        // 如果佇列滿了或者是非執行的任務都拒絕執行
        else if (!addWorker(command, false))
            reject(command);
    }

下面我們繼續看看addWorker是如何實現的:

  private boolean addWorker(Runnable firstTask, boolean core) {
        // java標籤
        retry:
        // 死迴圈
        for (;;) {
            int c = ctl.get();
            // 獲取當前執行緒狀態
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            // 這個邏輯判斷有點繞可以改成 
            // rs >= shutdown && (rs != shutdown || firstTask != null || workQueue.isEmpty())
            // 邏輯判斷成立可以分為以下幾種情況均不接受新任務
            // 1、rs > shutdown:--不接受新任務
            // 2、rs >= shutdown && firstTask != null:--不接受新任務
            // 3、rs >= shutdown && workQueue.isEmppty:--不接受新任務
            // 邏輯判斷不成立
            // 1、rs==shutdown&&firstTask != null:此時不接受新任務,但是仍會執行佇列中的任務
            // 2、rs==shotdown&&firstTask == null:會執行addWork(null,false)
            //  防止了SHUTDOWN狀態下沒有活動執行緒了,但是佇列裡還有任務沒執行這種特殊情況。
            //  新增一個null任務是因為SHUTDOWN狀態下,執行緒池不再接受新任務
            if (rs >= SHUTDOWN &&! (rs == SHUTDOWN && firstTask == null &&! workQueue.isEmpty()))
                return false;
            // 死迴圈
            // 如果執行緒池狀態為RUNNING並且佇列中還有需要執行的任務
            for (;;) {
                // 獲取執行緒池中執行緒數量
                int wc = workerCountOf(c);
                // 如果超出容量或者最大執行緒池容量不在接受新任務
                if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 執行緒安全增加工作執行緒數
                if (compareAndIncrementWorkerCount(c))
                    // 跳出retry
                    break retry;
                c = ctl.get();  // Re-read ctl
                // 如果執行緒池狀態發生變化,重新迴圈
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        // 走到這裡說明工作執行緒數增加成功
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            final ReentrantLock mainLock = this.mainLock;
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                // 加鎖
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int c = ctl.get();
                    int rs = runStateOf(c);
                    // RUNNING狀態 || SHUTDONW狀態下清理佇列中剩餘的任務
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        // 檢查執行緒狀態
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        // 將新啟動的執行緒新增到執行緒池中
                        workers.add(w);
                        // 更新執行緒池執行緒數且不超過最大值
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                // 啟動新新增的執行緒,這個執行緒首先執行firstTask,然後不停的從佇列中取任務執行
                if (workerAdded) {
                    //執行ThreadPoolExecutor的runWoker方法
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            // 執行緒啟動失敗,則從wokers中移除w並遞減wokerCount
            if (! workerStarted)
                // 遞減wokerCount會觸發tryTerminate方法
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorker之後是runWorker,第一次啟動會執行初始化傳進來的任務firstTask;然後會從workQueue中取任務執行,如果佇列為空則等待keepAliveTime這麼長時間

 final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        // 允許中斷
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // 如果getTask返回null那麼getTask中會將workerCount遞減,如果異常了這個遞減操作會在processWorkerExit中處理
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

我們看下getTask是如何執行的

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
        // 死迴圈
        retry: for (;;) {
            // 獲取執行緒池狀態
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            // 1.rs > SHUTDOWN 所以rs至少等於STOP,這時不再處理佇列中的任務
            // 2.rs = SHUTDOWN 所以rs>=STOP肯定不成立,這時還需要處理佇列中的任務除非佇列為空
            // 這兩種情況都會返回null讓runWoker退出while迴圈也就是當前執行緒結束了,所以必須要decrement
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                // 遞減workerCount值
                decrementWorkerCount();
                return null;
            }
            // 標記從佇列中取任務時是否設定超時時間
            boolean timed; // Are workers subject to culling?
            // 1.RUNING狀態
            // 2.SHUTDOWN狀態,但佇列中還有任務需要執行
            for (;;) {
                int wc = workerCountOf(c);
                // 1.core thread允許被超時,那麼超過corePoolSize的的執行緒必定有超時
                // 2.allowCoreThreadTimeOut == false && wc >
                // corePoolSize時,一般都是這種情況,core thread即使空閒也不會被回收,只要超過的執行緒才會
                timed = allowCoreThreadTimeOut || wc > corePoolSize;
                // 從addWorker可以看到一般wc不會大於maximumPoolSize,所以更關心後面半句的情形:
                // 1. timedOut == false 第一次執行迴圈, 從佇列中取出任務不為null方法返回 或者
                // poll出異常了重試
                // 2.timeOut == true && timed ==
                // false:看後面的程式碼workerQueue.poll超時時timeOut才為true,
                // 並且timed要為false,這兩個條件相悖不可能同時成立(既然有超時那麼timed肯定為true)
                // 所以超時不會繼續執行而是return null結束執行緒。
                if (wc <= maximumPoolSize && !(timedOut && timed))
                    break;
                // workerCount遞減,結束當前thread
                if (compareAndDecrementWorkerCount(c))
                    return null;
                c = ctl.get(); // Re-read ctl
                // 需要重新檢查執行緒池狀態,因為上述操作過程中執行緒池可能被SHUTDOWN
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
            try {
                // 1.以指定的超時時間從佇列中取任務
                // 2.core thread沒有超時
                Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;// 超時
            } catch (InterruptedException retry) {
                timedOut = false;// 執行緒被中斷重試
            }
        }
    }

下面我們看下processWorkerExit是如何工作的

private void processWorkerExit(Worker w, boolean completedAbruptly) {
        // 正常的話再runWorker的getTask方法workerCount已經被減一了
        if (completedAbruptly)
            decrementWorkerCount();
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 累加執行緒的completedTasks
            completedTaskCount += w.completedTasks;
            // 從執行緒池中移除超時或者出現異常的執行緒
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }
        // 嘗試停止執行緒池
        tryTerminate();
        int c = ctl.get();
        // runState為RUNNING或SHUTDOWN
        if (runStateLessThan(c, STOP)) {
            // 執行緒不是異常結束
            if (!completedAbruptly) {
                // 執行緒池最小空閒數,允許core thread超時就是0,否則就是corePoolSize
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                // 如果min == 0但是佇列不為空要保證有1個執行緒來執行佇列中的任務
                if (min == 0 && !workQueue.isEmpty())
                    min = 1;
                // 執行緒池還不為空那就不用擔心了
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            // 1.執行緒異常退出
            // 2.執行緒池為空,但是佇列中還有任務沒執行,看addWoker方法對這種情況的處理
            addWorker(null, false);
        }
    }

tryTerminate

processWorkerExit方法中會嘗試呼叫tryTerminate來終止執行緒池。這個方法在任何可能導致執行緒池終止的動作後執行:比如減少wokerCount或SHUTDOWN狀態下從佇列中移除任務。

final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            // 以下狀態直接返回:
            // 1.執行緒池還處於RUNNING狀態
            // 2.SHUTDOWN狀態但是任務佇列非空
            // 3.runState >= TIDYING 執行緒池已經停止了或在停止了
            if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && !workQueue.isEmpty()))
                return;
            // 只能是以下情形會繼續下面的邏輯:結束執行緒池。
            // 1.SHUTDOWN狀態,這時不再接受新任務而且任務佇列也空了
            // 2.STOP狀態,當呼叫了shutdownNow方法
            // workerCount不為0則還不能停止執行緒池,而且這時執行緒都處於空閒等待的狀態
            // 需要中斷讓執行緒“醒”過來,醒過來的執行緒才能繼續處理shutdown的訊號。
            if (workerCountOf(c) != 0) { // Eligible to terminate
                // runWoker方法中w.unlock就是為了可以被中斷,getTask方法也處理了中斷。
                // ONLY_ONE:這裡只需要中斷1個執行緒去處理shutdown訊號就可以了。
                interruptIdleWorkers(ONLY_ONE);
                return;
            }
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // 進入TIDYING狀態
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        // 子類過載:一些資源清理工作
                        terminated();
                    } finally {
                        // TERMINATED狀態
                        ctl.set(ctlOf(TERMINATED, 0));
                        // 繼續awaitTermination
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }

shutdown這個方法會將runState置為SHUTDOWN,會終止所有空閒的執行緒。shutdownNow方法將runState置為STOP。和shutdown方法的區別,這個方法會終止所有的執行緒。主要區別在於shutdown呼叫的是interruptIdleWorkers這個方法,而shutdownNow實際呼叫的是Worker類的interruptIfStarted方法:

他們的實現如下:

public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 執行緒池狀態設為SHUTDOWN,如果已經至少是這個狀態那麼則直接返回
            advanceRunState(SHUTDOWN);
            // 注意這裡是中斷所有空閒的執行緒:runWorker中等待的執行緒被中斷 → 進入processWorkerExit →
            // tryTerminate方法中會保證佇列中剩餘的任務得到執行。
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        // STOP狀態:不再接受新任務且不再執行佇列中的任務。
        advanceRunState(STOP);
        // 中斷所有執行緒
        interruptWorkers();
        // 返回佇列中還沒有被執行的任務。
        tasks = drainQueue();
    }
    finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}
private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            // w.tryLock能獲取到鎖,說明該執行緒沒有在執行,因為runWorker中執行任務會先lock,
            // 因此保證了中斷的肯定是空閒的執行緒。
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    }
    finally {
        mainLock.unlock();
    }
}
void interruptIfStarted() {
    Thread t;
    // 初始化時state == -1
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
        try {
            t.interrupt();
        } catch (SecurityException ignore) {
        }
    }
}

執行緒池的使用

執行緒池的建立

我們可以通過ThreadPoolExecutor來建立一個執行緒池

    /**
     * @param corePoolSize 執行緒池基本大小,核心執行緒池大小,活動執行緒小於corePoolSize則直接建立,大於等於則先加到workQueue中,
     * 佇列滿了才建立新的執行緒。當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,
     * 等到需要執行的任務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads()方法,
     * 執行緒池會提前建立並啟動所有基本執行緒。
     * @param maximumPoolSize 最大執行緒數,超過就reject;執行緒池允許建立的最大執行緒數。如果佇列滿了,
     * 並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務
     * @param keepAliveTime
     * 執行緒池的工作執行緒空閒後,保持存活的時間。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高執行緒的利用率
     * @param unit  執行緒活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、
     * 毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)
     * @param workQueue 工作佇列,執行緒池中的工作執行緒都是從這個工作佇列源源不斷的獲取任務進行執行
     */
    public ThreadPoolExecutor(int corePoolSize,
               int maximumPoolSize,
               long keepAliveTime,
               TimeUnit unit,
               BlockingQueue<Runnable> workQueue) {
        // threadFactory用於設定建立執行緒的工廠,可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                Executors.defaultThreadFactory(), defaultHandler);
    }

向執行緒池提交任務

可以使用兩個方法向執行緒池提交任務,分別為execute()和submit()方法。execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功。通過以下程式碼可知execute()方法輸入的任務是一個Runnable類的例項。

threadsPool.execute(new Runnable() {
        @Override
        public void run() {
        }
    });

submit()方法用於提交需要返回值的任務。執行緒池會返回一個future型別的物件,通過這個future物件可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完。

Future<Object> future = executor.submit(harReturnValuetask);
  try
    {
        Object s = future.get();
    }catch(
    InterruptedException e)
    {
        // 處理中斷異常
    }catch(
    ExecutionException e)
    {
        // 處理無法執行任務異常
    }finally
    {
        // 關閉執行緒池
        executor.shutdown();
    }

關閉執行緒池

可以通過呼叫執行緒池的shutdown或shutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫shutdown方法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow方法。

合理的配置執行緒池

要想合理地配置執行緒池,就必須首先分析任務特性,可以從以下幾個角度來分析。

1、任務的性質:CPU密集型任務、IO密集型任務和混合型任務。

2、任務的優先順序:高、中和低。

3、任務的執行時間:長、中和短。

4、任務的依賴性:是否依賴其他系統資源,如資料庫連線。

性質不同的任務可以用不同規模的執行緒池分開處理。CPU密集型任務應配置儘可能小的執行緒,如配置Ncpu+1個執行緒的執行緒池。由於IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如2*Ncpu。混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於序列執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數。優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。它可以讓優先順序高的任務先執行

如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行。執行時間不同的任務可以交給不同規模的執行緒池來處理,或者可以使用優先順序佇列,讓執行時間短的任務先執行。依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,等待的時間越長,則CPU空閒時間就越長,那麼執行緒數應該設定得越大,這樣才能更好地利用CPU。

建議使用有界佇列。有界佇列能增加系統的穩定性和預警能力,可以根據需要設大一點兒,比如幾千。有時候我們系統裡後臺任務執行緒池的佇列和執行緒池全滿了,不斷丟擲拋棄任務的異常,通過排查發現是資料庫出現了問題,導致執行SQL變得非常緩慢,因為後臺任務執行緒池裡的任務全是需要向資料庫查詢和插入資料的,所以導致執行緒池裡的工作執行緒全部阻塞,任務積壓線上程池裡。如果當時我們設定成無界佇列,那麼執行緒池的佇列就會越來越多,有可能會撐滿記憶體,導致整個系統不可用,而不只是後臺任務出現問題。當然,我們的系統所有的任務是用單獨的伺服器部署的,我們使用不同規模的執行緒池完成不同型別的任務,但是出現這樣問題時也會影響到其他任務。

執行緒池的監控

如果在系統中大量使用執行緒池,則有必要對執行緒池進行監控,方便在出現問題時,可以根據執行緒池的使用狀況快速定位問題。可以通過執行緒池提供的引數進行監控,在監控執行緒池的時候可以使用以下屬性

  • taskCount:執行緒池需要執行的任務數量。
  • completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
  • largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是否曾經滿過。如該數值等於執行緒池的最大大小,則表示執行緒池曾經滿過。
  • getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,執行緒池裡的執行緒不會自動銷燬,所以這個大小隻增不減。
  • getActiveCount:獲取活動的執行緒數。

通過擴充套件執行緒池進行監控。可以通過繼承執行緒池來自定義執行緒池,重寫執行緒池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和執行緒池關閉前執行一些程式碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。

相關文章