java多執行緒系列之執行緒池

recklessMo發表於2017-10-26

本篇主要從執行緒池的基本邏輯出發,然後深入研究了一些執行緒池的細節問題,弄清楚這些問題,才能更好的使用執行緒池。

第一:執行緒池基本邏輯

  1. 執行邏輯:使用執行緒池的虛擬碼如下。因為執行緒池最終是由執行緒來執行的,所以task還是需要實現runnable介面。
ThreadPool pool = new ThreadPool(100);
pool.execte(new Task());

class Task() implements Runnable{
  public void run(){
    ...
  }
}複製程式碼
  1. 管理方法:一個執行緒池是管理了很多個執行緒的一個物件, 所以不可避免的執行緒池還需要一系列的管理相關的方法,比如:執行緒池的執行緒怎麼啟動呢?執行緒池怎麼關閉呢(需要等待正在執行的任務結束嗎,如果任務長時間不結束怎麼辦)?我想獲取執行緒池的狀態怎麼辦(執行緒池現在跑的怎麼樣,有多少個執行緒在執行,執行了多少任務了,有多少任務在等待等等)?我想獲取執行的結果怎麼辦 (上一篇介紹過的future機制)?執行緒池中的執行緒一直存活著嗎還是需要keepalive timeout?所以不可避免的,我們需要加入如下管理方法
class ThreadPool{
  public static int STATUS_INIT = 1;
  public static int STATUS_RUN = 2;
  public static int STATUS_CLOSING = 3;
  public static int STATUS_CLOSED = 4;

  public void init();//初始化 1
  public void shutdown(); //關閉 2
  public Status getStatus();//獲取狀態 3
  public future execute();//執行 4
}複製程式碼
  1. 任務管理分配:我們想想怎麼執行緒池使用的具體邏輯:首先使用者向執行緒池通過execute方法提交任務task,然後執行緒池執行,最後使用者通過future機制獲取結果。有幾個問題:分配任務給執行緒去執行的邏輯,執行緒池有100個執行緒,來了任務之後怎麼分配,是輪詢的進行分配,還是隨機分配等等。執行緒池滿了之後再來任務我們應該怎麼辦?用佇列存放需要執行的任務還是直接拒絕?
  2. 實用feature:最後執行緒池需要有定時執行和週期執行的功能,以及批量執行等功能

上面這些都是執行緒池的一些必備的邏輯!很多文章都有提到,所以不深入進行分析。

第二:java執行緒池需要注意的細節

執行緒的提交階段:

注意當執行緒數目大於corePoolSize之後,是將任務放入到佇列之中,只有當佇列存滿了之後,如果此時執行緒數目小於maxPoolSize,才會新開執行緒進行執行! 這點需要注意,和我們直覺上有點差異!所以我們在使用的時候一定要根據負載來設定好佇列的長度,否則maxPoolSize這個引數就失效了。具體程式碼如下:

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        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. 如果執行緒數小於corePoolSize,那就新建一個執行緒來執行提交的任務,addWorker會原子的檢查runState和 workerCount,防止建立出多的執行緒。當建立執行緒失敗的時候返回false,比如兩個建立的動作同時操作,但是隻有一個能成功。

  2. 執行緒數大於corePoolSize,那麼就放入workQueue中,然後recheck一下,如果發現執行緒池不在執行態了,就從佇列中移除當前任務並且reject呼叫當前這個任務,目的就是防止任務不被執行,如果放進了佇列並且再次檢查執行緒池處於執行狀態,那麼就可以由執行緒池來確保該任務執行,提交者就不用擔心了。如果執行緒池在執行態但是worker數目為0(具體場景可能是corePoolSize被設定成0的情況),那麼就啟動新執行緒來執行,這個recheck就是為了確定加入佇列的任務確保能夠執行。

  3. 如果將任務新增到佇列失敗,那麼直接嘗試新增worker來執行當前任務,如果新增失敗說明執行緒池正在關閉中所以呼叫reject邏輯。這塊可能要我寫的話我就會將新增佇列失敗和執行緒池關閉兩部分分別處理。

執行緒執行異常的處理

執行緒異常的處理可能很多人都沒注意,如果我們在提交的runnable的run方法裡面沒有進行異常的處理怎麼辦?具體程式碼如下

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            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();//1
                    } 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);
        }
    }複製程式碼

可以看到task.run方法實際上是被try catch住的,但是在被catch住之後又會被丟擲來,所以如果我們在runnable的run方法裡面沒有處理異常的話實際上是會讓worker跳出獲取任務執行的迴圈,進而執行processWorkerExit方法的,如下

    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        tryTerminate();

        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }複製程式碼

核心邏輯就是做一些清理工作,把worker物件從worker集合中刪除掉,worker對應的執行緒在出現異常後執行完上面的processWorkerExit方法之後就自動結束了,但是會執行thread物件裡面uncaughtexceptionhandler。因為在jvm中如果一個執行緒的執行丟擲了異常並且沒有被捕獲的話,那麼該執行緒的執行會被終止,然後jvm會檢視對應thread的uncaughtexceptionhandler來進行處理,所以可以通過set這個handler來進行個性化的異常定製,算是對異常的最後一道防線吧。所以如果出現了異常,執行緒池的執行緒就會死亡,然後下一次任務過來的時候就會重新建立執行緒!

worker實現了aqs,後面會分析aqs類(TODO),這是一大利器

    private final class Worker extends AbstractQueuedSynchronizer implements Runnable複製程式碼

可以看到worker是aqs的子類,並且worker的初始的state設定成了-1, 這樣在初始化的過程中就沒有辦法中斷這個worker,直到runworker的時候將state設定成0之後才能進行中斷worker。中斷worker呼叫的是下面這個方法:必須要state>=0才能進行中斷。這也比較好理解,一般中斷執行緒就是想結束執行緒的執行,是想讓執行緒從任務的獲取-執行這一邏輯中進行中斷,所以worker剛開始建立的時候就不允許中斷,要不然建立就會失敗。

        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }複製程式碼

另外實現aqs的目的是在執行task的時候進行加鎖,加鎖的目的是為了防止task的執行時候修改了corepoolsize的數量或者呼叫別的某些方法導致需要中斷worker的情況,程式碼如下:

            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();
                }
            }複製程式碼
    public void setCorePoolSize(int corePoolSize) {
        if (corePoolSize < 0)
            throw new IllegalArgumentException();
        int delta = corePoolSize - this.corePoolSize;
        this.corePoolSize = corePoolSize;
        if (workerCountOf(ctl.get()) > corePoolSize)
            interruptIdleWorkers();
        else if (delta > 0) {
            // We don't really know how many new threads are "needed".
            // As a heuristic, prestart enough new workers (up to new
            // core size) to handle the current number of tasks in
            // queue, but stop if queue becomes empty while doing so.
            int k = Math.min(delta, workQueue.size());
            while (k-- > 0 && addWorker(null, true)) {
                if (workQueue.isEmpty())
                    break;
            }
        }
    }複製程式碼

如果corePoolSize數量變小了,那麼就需要interrupt掉空閒的worker,interruptIdleWorkers方法如下:

    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }複製程式碼

可以看到如果執行緒沒有被中斷,並且能夠獲取worker的lock,這個時候才去中斷執行緒。如果worker在執行任務的過程中,那麼worker是會持有aqs這個獨佔鎖的,所以tryLock會返回失敗,所以就不能中斷正在執行task的任務。這也是符合邏輯的。也就是不能中斷正在執行的worker。

執行緒池如何關閉

void shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown(); // Disable new tasks from being submitted
    try {
      // Wait a while for existing tasks to terminate
      if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
        pool.shutdownNow(); // Cancel currently executing tasks
        // Wait a while for tasks to respond to being cancelled
        if (!pool.awaitTermination(60, TimeUnit.SECONDS))
            System.err.println("Pool did not terminate");
      }
    } catch (InterruptedException ie) {
      // (Re-)Cancel if current thread also interrupted
      pool.shutdownNow();
      // Preserve interrupt status
      Thread.currentThread().interrupt();
    }
 }複製程式碼

這是文件給出的比較推薦的解決辦法。但是一般需要自己根據業務邏輯來進行分析,找到合適的優雅的解決辦法。

執行緒池任務佇列

任務佇列一般分為三種,一種是無界佇列比如linkedblockingqueue,一種是有界佇列比如arrayblockingqueue,一種是同步佇列SynchronousQueue

同步佇列會單獨詳解TODO

總結:使用執行緒池要注意到各個引數的含義,以及內部的實現,這樣使用起來才有底

相關文章