深入淺出Java執行緒池ThreadPoolExecutor

江溢Jonny發表於2018-03-16

簡書江溢Jonny,轉載請註明原創出處,謝謝!

本文基於JDK1.7的原始碼進行分析並解讀。我們將以一個簡單的案例開始本話題的討論,並且在文章的結尾,筆者將會給出一些經驗之談,避免後來者踩坑。

如果你喜歡我,可以關注我的公眾號~更多幹貨~

深入淺出Java執行緒池ThreadPoolExecutor

ThreadPoolExecutor是JUC提供的一類執行緒池工具,也是Java語言中應用場景最多的併發框架,可以說,幾乎所有需要非同步或者併發執行的,都可以使用Java執行緒池。那麼首先,我們一起來比較一下“單純使用執行緒的方案”和“使用ThreadPoolExecutor執行緒池的方案”,在解決問題上有什麼區別吧。

案例:抄寫員

在中世紀,有一種叫做抄寫員(Scribe)的工作,他們的職責就好像是影印機,抄寫一本又一本書。假如這個時候有一個抄寫員工作室,只有2個抄寫員,他們要抄寫10本書。

我們在本例中分別“自己寫執行緒管理”和“由ThreadPoolExecutor做執行緒管理”

public static class Book {
 
    private static AtomicInteger id = new AtomicInteger(0); // 書名生成器
    private String bookName; // 書名
     
    public void copy() { // 抄寫書籍
        System.out.println("start copy " + bookName);
        try {
            Thread.sleep(100L); // sleep 100ms
        } catch (Exception e) {
            // ignore
        }
        System.out.println("end copy " + bookName);
    }
 
    public Book() {
        bookName = "book-" + String.valueOf(id.incrementAndGet()); // 書名自動生成
    }
}
複製程式碼

自己實現執行緒管理

// 提前準備好十本書
final BlockingQueue<Book> books = new LinkedBlockingDeque<Book>(10);
for (int i = 0; i < 10; i++) {
    try {
        books.put(new Book());
    } catch (Exception e) {
        // ignore
    }
}
 
 
System.out.println("start work...");
// 建立兩個書籍抄寫員執行緒
Thread[] scribes = new Thread[2];
for (int scribeIndex = 0; scribeIndex < 2; scribeIndex++) {
    scribes[scribeIndex] = new Thread(new Runnable() {
        public void run() {
            for (; ; ) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("time arrives, stop writing...");
                }
                try {
                    Book currentBook = books.poll(5, TimeUnit.SECONDS);
                    currentBook.copy();
                } catch (Exception e) {
                    System.out.println("time arrives, stop writing...");
                    return;
                }
            }
        }
    });
    scribes[scribeIndex].setDaemon(false); // 設定為非守護執行緒
    scribes[scribeIndex].start();
}
 
// 工作已經安排下去了,安心等待就好了
try {
    Thread.sleep(10000l);
} catch (Exception e) {
    // ignore
}
 
// 時間到了,提醒兩個抄寫員停止抄寫
for (int scribeIndex = 0; scribeIndex < 2; scribeIndex++) {
    scribes[scribeIndex].interrupt();
}
 
System.out.println("end work...");
複製程式碼

寫了一大堆程式碼來完成上述的功能,讓我們一起來看看用了ThreadPoolExecutor是怎麼完成的。

System.out.println("start work...");
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i ++) {
    executorService.submit(new Runnable() {
        public void run() {
            new Book().copy();
        }
    });
}
 
// 工作已經安排下去了,安心等待就好了
try {
    Thread.sleep(10000l);
} catch (Exception e) {
    // ignore
}
 
executorService.shutdownNow();
System.out.println("end work...");
複製程式碼

整個流程非常清晰,分別是:任務編寫執行緒建立執行緒啟動終止執行緒

但是很多時候,問題並不僅限於上述的內容。

開發者困境

最早的併發程式設計的開發者很多事情都需要親力親為,而通過使用Java執行緒池,可以完成以下工作:

1)執行緒管理,執行緒的建立、啟動、銷燬等工作;

2)執行緒複用,執行緒的建立是會給伺服器帶來一定開銷的,如何減少頻繁重複建立執行緒的開銷;

3)彈性伸縮,伺服器通常有高峰期也有低峰期,執行緒池是否可以彈性伸縮,比如執行緒建立成功後長時間不使用是否可以回收,以減少系統資源的浪費,或者執行緒池的容量是否可以隨時增長;

4)拒絕策略,執行緒數量有限而需要處理的任務很多,超出系統承載範圍的任務是拒絕還是阻塞等待;

5)異常處理,執行緒在執行過程中可能遇到異常或者錯誤,開發者如何正確應對這些異常或者錯誤;

6)任務分配,任務的分配是基於先入先出還是基於某種優先順序的策略。

等等如是,不一而足,這個時候我們就要介紹Doug Lea大神開發的ThreadPoolExecutor執行緒池框架,看看大神是怎麼解決上面這些問題的。

ThreadPoolExecutor原始碼簡析

首先,在解讀原始碼之前,要引入ThreadPoolExecutor的一些重要概念

生命週期

在ThreadPoolExecutor執行緒池的設計中,把整個任務執行框架執行緒池劃分為5個生命週期:

RUNNING:允許接收新任務並且處理佇列中的任務

SHUTDOWN:不再接收新的任務,僅消化完佇列中的任務

STOP:不僅不再接收新的任務,連佇列中的任務都不再消化處理了,並且嘗試中斷正在執行任務的執行緒

TIDYING:所有任務被終止了,工作執行緒數workCount也被設為0,執行緒的狀態也被設為TIDYING,並開始呼叫鉤子函式terminated()

TERMINATED:鉤子函式terminated()執行完畢

各個生命週期的轉化圖如下:

執行緒池的生命週期

從圖中可以看到,整個生命週期的變化是不可逆的。

狀態字

ThreadPoolExecutor把執行緒池狀態和執行緒池容量打包成一個int型變數,如下圖所示

狀態字

執行緒池狀態位

狀態 高位值列舉 正負性
RUNNING 111 負數(-536870912)
SHUTDOWN 000 0
STOP 001 正數(536870912)
TIDYING 010 正數(1073741824)
TERMINATED 011 正數(1610612736)

因此在狀態值的排布上可以知道 TERMINATED > TIDYING > STOP >SHUTDOWN > RUNNING

ThreadPoolExecutor中的程式碼如下所示:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
 
// 狀態字的高位元位存放執行緒池狀態資訊
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;
 
// 打包/提取狀態字資訊
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
  
// 判斷當前執行緒池是否正在執行
private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}
複製程式碼

執行緒池主要執行流程

執行緒池主要執行流程

程式碼介紹

首先,我們建立一個執行緒池。

1.執行緒池建立
ExecutorService executorService = Executors.newFixedThreadPool(2);
複製程式碼

這裡使用了Executors提供的工廠方法,可以建立以下四種型別執行緒池:

newFixedThreadPool。該方法將用於建立一個固定大小的執行緒池(此時corePoolSize = maxPoolSize),每提交一個任務就建立一個執行緒池,直到執行緒池達到最大數量,執行緒池的規模在此後不會發生任何變化;

newCachedThreadPool。該方法建立了一個可快取的執行緒池,(此時corePoolSize = 0,maxPoolSize = Integer.MAX_VALUE),空閒執行緒超過60秒就會被自動回收,該執行緒池存在的風險是,如果伺服器應用達到請求高峰期時,會不斷建立新的執行緒,直到記憶體耗盡;

newSingleThreadExecutor。該方法建立了一個單執行緒的執行緒池,該執行緒池按照任務在佇列中的順序序列執行(如:FIFOLIFO、優先順序);

newScheduledThreadPool。該方法建立了一個固定長度的執行緒池,可以以延遲或者定時的方式執行任務;

2、任務提交

任務提交的大概邏輯如下:

1)當執行緒池小於corePoolSize時,新提交任務將建立一個新執行緒執行任務,即使此時執行緒池中存在空閒執行緒;

2)當執行緒池達到corePoolSize時,新提交任務將被放入workQueue中,等待執行緒池中任務排程執行;

3)當workQueue已滿,且maximumPoolSize > corePoolSize時,新提交任務會建立新執行緒執行任務;

4)當提交任務數超過maximumPoolSize時,新提交任務由RejectedExecutionHandler處理;

5)當執行緒池中超過corePoolSize執行緒,空閒時間達到keepAliveTime時,關閉空閒執行緒;

那麼接下來我們看看原始碼是怎麼實現上面的描述的

執行緒池建立成功以後,我們提交任務到執行緒池中:

executorService.submit(new Runnable() {
    public void run() {
        new Book().copy();
    }
});
複製程式碼

submit到執行緒池以後:

public <T> Future<T> submit(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);// 包裝出一個新的任務
    execute(ftask); // 執行緒池的入口
    return ftask;
}
複製程式碼

可以看到ThreadPoolExecutor的入口方法就是execute(Runnable commad)。該方法的執行邏輯如下:

int c = ctl.get();
// 1. 如果當前執行緒池中執行緒總數少於核心執行緒數,則新增新執行緒到執行緒池中,
// 並且由新執行緒執行剛提交進來的任務
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}
  
if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    // 2. 可能剛才在建立新執行緒成功的同時,執行緒池被關閉了,因此需要double-check,
    // 如果此時執行緒池已經被關閉了,那麼回滾剛才被新增進來的任務
    if (! isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}
// 3. 如果此時核心執行緒數(corePoolSize)已經滿了,並且任務佇列也滿了,
// 嘗試增加執行緒到maximumPoolSize大小,如果仍然失敗,執行拒絕策略
else if (!addWorker(command, false))
    reject(command);
複製程式碼

在上面的程式碼裡面,ctl.get()方法、workerCountOf()、以及isRunning() 方法都是對之前提到的狀態字進行讀寫的操作,這部分我們就不再展開給讀者看了,有興趣的讀者可以自己瞭解一下。

接下來,我們看看addWorker都做了什麼工作:

private boolean addWorker(Runnable firstTask, boolean core) {
    // 這部分省略的程式碼都是對狀態字進行修改,新增並建立執行緒之前,
    // 需要遞增work記數(此時需要執行緒安全地操作)
    ...
 
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        ...
        w = new Worker(firstTask); // 此處封裝出了一個新的Work,這個類我們稍後會介紹
        final Thread t = w.thread;
        if (t != null) {
            ...
             
                // 獲得執行緒池狀態,如果執行緒池已經被關閉了,則不再建立新的執行緒
                int c = ctl.get();
                int rs = runStateOf(c);
 
                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();
                    ...
                    workerAdded = true;
            ...
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            // 如果任務啟動或者提交到執行緒池失敗,
            // 則執行回滾操作(從工作執行緒池中移除失敗新增的worker、減少狀態字中的任務計數)
            addWorkerFailed(w);
    }
    return workerStarted;
}

複製程式碼
3、任務執行

任務執行在Worker類中,而Worker類是一個繼承了Runnable介面的類。

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    ...
    public void run() {
        runWorker(this);
    }
    ...
}
複製程式碼

可以看到Worker類中呼叫了外部的runWorker()方法。因此可以瞭解到,任務執行的主要邏輯,就是在外部的runWorker()方法中執行的

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    ...
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) { // 迴圈讀取任務
            ...
            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++;
                ...
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

複製程式碼

beforeExecute和afterExecute是兩個鉤子方法,在裡面指定了當執行緒開始執行和完成執行以後執行的動作,需要開發者實現。

另外需要注意的還有runWorker方法內呼叫的getTask()方法,在該方法內部,如果發生以下情況將會返回null,終止工作執行緒的執行迴圈: 1)當前執行緒數即將超過maxPoolSize 2)執行緒池被關閉 3)當前執行緒數大於corePoolSize且小於maxPoolSize,並關切從BlockingQueue取資料超過了超時時間(預設60秒)

程式碼實現如下:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
 
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
 
        // 校驗當前執行緒池狀態
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
 
        boolean timed;      // Are workers subject to culling?
 
        for (;;) {
            int wc = workerCountOf(c);
            timed = allowCoreThreadTimeOut || wc > corePoolSize;
 
            if (wc <= maximumPoolSize && ! (timedOut && timed))
                break;
            if (compareAndDecrementWorkerCount(c))
                return null;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
 
        try {
            // 如果執行緒超過指定時間內(預設60秒)沒有獲取到任務,說明有執行緒即將過期
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

複製程式碼
4、任務拒絕

如果執行緒被提交到執行緒池時,當前執行緒池出現以下情況的任一一種情況: 1)執行緒池任務佇列已經滿了 2)執行緒池被關閉了(呼叫了shutdown函式或者shutdownNow函式) 都將會呼叫提前設定好的回撥策略,ThreadPoolExecutor中總共提供了四種策略:

1)AbortPolicy(中止):該策略將會直接丟擲RejectedExecutionException異常,呼叫者將會獲得異常;

2)DiscardPolicy(拋棄):使用該策略,執行緒池將會悄悄地丟棄這個任務而不被呼叫者知道;

3)CallerRunsPolicy(呼叫者執行):該策略既不會拋棄任務也不會丟擲異常,而是將這個任務退回給呼叫者,從而降低新任務的流量;

4)DiscardOldestPolicy(拋棄最舊的):該策略將會拋棄下一個即將輪到執行的任務,那麼“拋棄最舊”的將導致拋棄優先順序最高的任務,因此最好不要把“拋棄最舊的”飽和策略和優先順序佇列放在一起使用; 這裡,程式碼實現我們將只展示**CallerRunsPolicy(呼叫者執行)**策略:

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() { }
     
    // 策略實現
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

複製程式碼

當然開發者也可以選擇,根據業務需求,定義自己的飽和策略。

5、執行緒池銷燬

ThreadPoolExecutor提供了兩種方法銷燬執行緒池,分別是shutdown()shutdownNow()

shutdown()方法僅僅是把執行緒池的狀態置為SHUTDOWN,並且拒絕之後嘗試提交進來的所有請求,但是已經在任務佇列裡的任務會仍然會正常消費。

shutdownNow()方法的表現顯得更加簡單粗暴,它會強行關閉ExecutorService,也會嘗試取消正在執行的任務,並且返回所有已經提交但尚未開始的任務,開發者可以將這些任務寫入日誌儲存起來以便之後進行處理,另外嘗試取消正在執行的任務僅僅是嘗試對執行執行緒進行中斷,具體的執行緒響應中斷策略需要使用者自己編寫。程式碼實現如下:

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}
複製程式碼

小心踩坑:執行緒池經驗談

不要使用ThreadLocal

不要在ThreadPoolExecutor執行緒池中使用ThreadLocal,因為在ThreadPoolExecutor中,執行緒是複用的,因此在這裡使用ThreadLocal會被多個task共享,因此可能會帶來髒資料汙染。需要小心使用

合理設定corePoolSize的值

以一段程式碼為例:

// 10個執行緒,因為任務多,這裡用LinkedBlockingQueue
private static final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
private static final ExecutorService service = new ThreadPoolExecutor(0, 10,
                                                60L, TimeUnit.SECONDS, queue
);
複製程式碼

程式碼中的corePoolSize=0,也就是核心執行緒數是1,如果任務數多於10個,那麼會先建立maximumPoolSize個執行緒執行,其餘的任務加入 queue 中等待執行。

而在ThreadPoolExecutor的實現中,當workQueue已滿,且maximumPoolSize>corePoolSize時,新提交任務會建立新執行緒執行任務。

因此,queue 是不會滿的,那麼永遠不會有maximumPoolSize個執行緒被建立,也就是說我們的任務一直還是一個執行緒在跑,無法達到可以同時使用多個執行緒的預期。

執行緒中斷

雖然ThreadPoolExecutor提供了shutdownNow()方法,在呼叫該方法後會嘗試中斷所有執行緒,但是該中斷並不能保證執行緒一定會就此終止,因此,需要開發者實現執行緒中斷的策略。關於這部分的內容,在Doug Lea的《Java Concurrency In Practice》的7.1.2節已經進行了完整的討論,筆者在這裡就不再贅述了。

finalize函式

尤其需要注意的是,ThreadPoolExecutor有一個finalize函式,具體實現如下:

protected void finalize() {
    shutdown();
}
複製程式碼

在該方法中呼叫了shutdown()函式,因此,如果你並不是真正希望停止執行緒池的執行,那麼就不要讓執行緒池離開你程式碼的作用範圍。

我是江溢Jonny。

深入淺出Java執行緒池ThreadPoolExecutor

相關文章