Java併發系列 — 執行緒池

牛覓發表於2019-01-04

Java中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池。

在開發過程中,合理地使用執行緒池能夠帶來3個好處:

  • 降低資源消耗:通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。

  • 提高響應速度:當任務到達時,任務可以不需要等到執行緒建立就能立即執行。

  • 提高執行緒的可管理性:執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。但是,要做到合理利用執行緒池,必須對其實現原理了如指掌。

如何建立執行緒池

1、使用Executors工廠類提供的靜態方法來建立執行緒池,方法如下:

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

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

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

2、通過ThreadPoolExecutor的建構函式建立,程式碼如下:


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.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
複製程式碼

引數說明:

  • 1、corePoolSize

    執行緒池中的核心執行緒數,當提交一個任務時,執行緒池建立一個新執行緒執行任務,直到當前執行緒數等於
    corePoolSize;如果當前執行緒數為corePoolSize,繼續提交的任務被儲存到阻塞佇列中,等待被執行;如果執行了執行緒池的prestartAllCoreThreads()方法,執行緒池會提前建立並啟動所有核心執行緒。

  • 2、maximumPoolSize

    執行緒池中允許的最大執行緒數。如果當前阻塞佇列滿了,且繼續提交任務,則建立新的執行緒執行任務,前提是當前執行緒數小於maximumPoolSize。

  • 3、keepAliveTime

    執行緒空閒時的存活時間,即當執行緒沒有任務執行時,繼續存活的時間;預設情況下,該引數只線上程數大於corePoolSize時才有用。

  • 4、unit

    keepAliveTime的單位

  • 5、workQueue

    用來儲存等待被執行的任務的阻塞佇列,且任務必須實現Runable介面。

    在JDK中提供瞭如下阻塞佇列:
    ArrayBlockingQueue:基於陣列結構的有界阻塞佇列,按FIFO排序任務;

    LinkedBlockingQuene:基於連結串列結構的阻塞佇列,按FIFO排序任務,吞吐量通常要高於ArrayBlockingQuene;

    SynchronousQuene:一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQuene;

    priorityBlockingQuene:具有優先順序的無界阻塞佇列;

  • 6、threadFactory

    建立執行緒的工廠,通過自定義的執行緒工廠可以給每個新建的執行緒設定一個具有識別度的執行緒名。

  • 7、handler

    執行緒池的飽和策略,當阻塞佇列滿了,且沒有空閒的工作執行緒,如果繼續提交任務,必須採取一種策略處理該任務,執行緒池提供了4種策略:

    1. AbortPolicy:直接丟擲異常,預設策略;
    2. CallerRunsPolicy:用呼叫者所在的執行緒來執行任務;
    3. DiscardOldestPolicy:丟棄阻塞佇列中靠最前的任務,並執行當前任務;
    4. DiscardPolicy:直接丟棄任務;

    當然也可以根據應用場景實現RejectedExecutionHandler介面,自定義飽和策略,如記錄日誌或持久化儲存不能處理的任務。

使用示例

public class ThreadPoolExecutorExample {

    // 1、通過threadPoolExecutor的建構函式建立執行緒池
    private static ThreadPoolExecutor threadPoolExecutor = 
            new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS,
                                    new ArrayBlockingQueue<Runnable>(10));


    public static void main(String[] args)
                throws ExecutionException, InterruptedException {

        // 2、使用execute方法執行沒有返回結果的任務
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                new Task().doSomething();
            }
        });

        // 3、使用submit方法執行有返回結果的任務且需要實現Callable介面
        Future future = threadPoolExecutor.submit(new Callable<Integer>() {
              @Override
              public Integer call() throws Exception {
                  return new Task().doOtherthing();
              }
          }
        );
        System.out.println(future.get());
    }
}

class Task {

    public void doSomething() {
        System.out.println("doSomeThing ...");
    }

    public int doOtherthing() {
        System.out.println("doOtherthing ..., and return 10.");
        return 10;
    }
}
複製程式碼

execute方法和submit方法的區別

  • execute方法

    用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功。

  • submit方法

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

ThreadPoolExecutor的實現原理

當向執行緒池提交一個任務之後,執行緒池是如何處理這個任務的呢?執行緒池的主要處理流程如下:

ThreadPoolExecutor的實現原理
  1. 如果當前執行的執行緒小於corePoolSize,則建立新執行緒來執行;
  2. 如果執行的執行緒等於或多於corePoolSize,則將任務加入BlockingQueue;
  3. 如果無法將任務加入BlockingQueue(佇列已滿),則建立新的執行緒來處理任務;
  4. 如果建立新執行緒將使當前執行的執行緒超出maximumPoolSize,任務將被拒絕,並呼叫handler.rejectedExecution(command, this)方法。

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

ThreadPoolExecutor原始碼分析

類結構圖

ThreadPoolExecutor類結構圖.png

核心變數與方法(狀態轉換)

// 初始化狀態和數量,狀態為RUNNING,執行緒數為0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 前3位表示狀態,所有執行緒數佔29位
private static final int COUNT_BITS = Integer.SIZE - 3;
// 執行緒池容量大小為 1 << 29 - 1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 執行緒池狀態
// RUNNING狀態:11100000000000000000000000000000(前3位為111)
private static final int RUNNING    = -1 << COUNT_BITS;
// SHUTDOWN狀態:00000000000000000000000000000000(前3位為000)
private static final int SHUTDOWN   =  0 << COUNT_BITS;
// STOP狀態:00100000000000000000000000000000(前3位為001)
private static final int STOP       =  1 << COUNT_BITS;
// TIDYING狀態:01000000000000000000000000000000(前3位為010)
private static final int TIDYING    =  2 << COUNT_BITS;
// TERMINATED狀態:01100000000000000000000000000000(前3位為011)
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 int ctlOf(int rs, int wc) { return rs | wc; }

複製程式碼

ThreadPoolExecutor執行緒池有5個狀態,分別是:

  1. RUNNING:可以接受新的任務,也可以處理阻塞佇列裡的任務。
  2. SHUTDOWN:不接受新的任務,但是可以處理阻塞佇列裡的任務。
  3. STOP:不接受新的任務,不處理阻塞佇列裡的任務,中斷正在處理的任務。
  4. TIDYING:過渡狀態,也就是說所有的任務都執行完了,當前執行緒池已經沒有有效的執行緒,這個時候執行緒池的狀態將會TIDYING,並且將要呼叫terminated方法。
  5. TERMINATED:終止狀態。terminated方法呼叫完成以後的狀態。

執行緒池的狀態轉換過程:

  • RUNNING -> SHUTDOWN
    On invocation of shutdown(), perhaps implicitly in finalize()
  • (RUNNING or SHUTDOWN) -> STOP
    On invocation of shutdownNow()
  • SHUTDOWN -> TIDYING
    When both queue and pool are empty
  • STOP -> TIDYING
    When pool is empty
  • TIDYING -> TERMINATED
    When the terminated() hook method has completed

Threads waiting in awaitTermination() will return when the state reaches TERMINATED.

核心方法

execute方法

使用ThreadPoolExecutor執行任務的時候,可以使用execute或submit方法,submit方法如下:

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
複製程式碼

通過原始碼可知submit方法同樣也是由execute()完成的,execute()方法原始碼如下:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 過程1
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    // 過程2
    if (isRunning(c) && workQueue.offer(command)) {
        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
        // 過程4
        reject(command);
}
複製程式碼

addWorker方法

addWorker方法的主要工作就是建立一個工作執行緒執行任務,程式碼如下:

/*
 * firstTask引數:用於指定新增的執行緒執行的第一個任務。
 * core為true:表示在新增執行緒時會判斷當前活動執行緒數是否少於corePoolSize,
 *      false表示新增執行緒前需要判斷當前活動執行緒數是否少於maximumPoolSize。
 */
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        /**
         * 只有當下面兩種情況會繼續執行,其他直接返回false(新增失敗)
         * 1、rs == RUNNING
         * 2、rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty() 
         *(執行了shutdown方法,但是阻塞佇列還有任務沒有執行)
         */
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
                firstTask == null &&
                ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            // 判斷工作執行緒的數量是否超過執行緒池的限制
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // workerCount加1成功,跳出兩層循壞。
            if (compareAndIncrementWorkerCount(c))
                break retry;
            /**
              * 能執行到這裡,都是因為多執行緒競爭,只有兩種情況
              * 1、workCount發生變化,compareAndIncrementWorkerCount失敗,
              *                         這種情況不需要重新獲取ctl,繼續for迴圈即可。
              * 2、runState發生變化,可能執行了shutdown或者shutdownNow,
              *                       這種情況重新走retry,取得最新的ctl並判斷狀態。
              */
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    // worker是否執行標識
    boolean workerStarted = false;
    // worker是否新增成功標識
    boolean workerAdded = false;
    // 儲存建立的worker變數
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        // 檢查執行緒是否建立成功
        if (t != null) {
            // 加鎖
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // 加鎖成功,重新檢查執行緒池的狀態 
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // 將w儲存到workers容器中
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    // 新增成功標識
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 執行任務
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            // 失敗回退,從 wokers 移除 w, 執行緒數減一,嘗試結束執行緒池(呼叫tryTerminate 方法)
            addWorkerFailed(w);
    }
    return workerStarted;
}
複製程式碼

Worker類

在分析t.start()之前,需要了解Worker類。其原始碼如下:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{

    // 工作執行緒
    final Thread thread;
    // 初始化任務
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;

    Worker(Runnable firstTask) {
        // 禁止中斷,直到runWorker
        setState(-1);
        this.firstTask = firstTask;
        // 很重要,worker例項被包裝成thread執行的任務。
        // 這樣t.start啟動後,將執行Worker的run方法。
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
        runWorker(this);
    }

    // 實現AQS的相關方法
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}
複製程式碼
runWorker方法
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();
             /*
              * 在執行任務之前先做一些處理。
              *  1. 如果執行緒池已經處於STOP狀態並且當前執行緒沒有被中斷,中斷執行緒。
              *  2. 如果執行緒池還處於RUNNING或SHUTDOWN狀態,並且當前執行緒已經被中斷了,
              *       重新檢查一下執行緒池狀態,如果處於* * STOP狀態並且沒有被中斷,那麼中斷執行緒。
              */
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();

            try {
                // hook method
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                     // 真正的開始執行任務,呼叫的是run方法,而不是start方法。
                    //  這裡run的時候可能會被中斷,比如執行緒池呼叫了shutdownNow方法
                    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 {
                    // hook method
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 回收woker
        processWorkerExit(w, completedAbruptly);
    }
}
複製程式碼
getTask方法
private Runnable getTask() {

    boolean timedOut = false;

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // 計算從佇列獲取任務的方式( poll or take)
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 當工作執行緒超過其最大值或者timed = true時其workQueue.isEmpty()時,返回null。
        // 這意味為該worker將被回收。
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
複製程式碼

由上可知,當allowCoreThreadTimeOut為true時,如果佇列長時間沒有任務,工作執行緒最終都會被銷燬。

其他知識點

關閉執行緒池

可以通過呼叫執行緒池的shutdownshutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。

但是它們存在一定的區別,shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。

合理地配置執行緒池

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

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

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

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

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

性質不同的任務可以用不同規模的執行緒池分開處理:

1)CPU密集型任務應配置儘可能小的執行緒,如配置N cpu +1個執行緒的執行緒池。

2)IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如2*N cpu 。

3)混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於序列執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。

優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。它可以讓優先順序高的任務先得到執行,需要注意的是如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行。

執行時間不同的任務可以交給不同規模的執行緒池來處理,或者也可以使用優先順序佇列,讓執行時間短的任務先執行。

依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,如果等待的時間越長CPU空閒時間就越長,那麼執行緒數應該設定越大,這樣才能更好的利用CPU。

可以通過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數

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

執行緒池監控

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

  • taskCount:執行緒池需要執行的任務數量。
  • completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
  • largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量。通過這個資料可以直到執行緒池是否曾經滿過。
  • getPoolSize:執行緒池的執行緒數量。
  • getActiveCount:獲取活動的執行緒數。

通過擴充套件執行緒池進行監控。可以通過繼承執行緒池來自定義執行緒池,重寫執行緒池的beforeExecuteafterExecuteterminated方法,也可以在任務執行前、執行後和執行緒池關閉之前執行一些程式碼來進行監控。

參考資料

  1. Java執行緒池ThreadPoolExecutor原始碼分析

  2. 【細談Java併發】談談執行緒池:ThreadPoolExecutor


如果讀完覺得有收穫的話,歡迎點贊、關注、加公眾號【牛覓技術】,查閱更多精彩歷史!!!

Java併發系列 — 執行緒池

相關文章