面試題-關於Java執行緒池一篇文章就夠了

secbro2發表於2019-12-16

在Java面試中,執行緒池相關知識,雖不能說是必問提,但出現的頻次也是非常高的。同時又鑑於公眾號“程式新視界”的讀者後臺留言讓寫一篇關於Java執行緒池的文章,於是就有本篇內容,本篇將基於Java執行緒池的原理、實現以及相關原始碼進行講解等。

什麼是執行緒池

執行緒池是一種多執行緒處理形式,處理過程中將任務提交到執行緒池,任務的執行交由執行緒池來管理。

為了充分利用CPU多核資源,應用都會採用多執行緒並行/併發計算,最大限度的利用多核提升應用程式效能。

試想一下,如果每個請求都執行一遍建立執行緒、執行任務、銷燬執行緒,那麼對伺服器資源將是一種浪費。在高併發的情況下,甚至會耗盡伺服器資源。

執行緒池的主要作用有兩個:不同請求之間重複利用執行緒,無需頻繁的建立和銷燬執行緒,降低系統開銷和控制執行緒數量上限,避免建立過多的執行緒耗盡程式記憶體空間,同時減少執行緒上下文切換次數。

常見面試題

  • 說說Java執行緒池的好處及實現的原理?
  • Java提供執行緒池各個引數的作用,如何進行的?
  • 根據執行緒池內部機制,當提交新任務時,有哪些異常要考慮?
  • 執行緒池都有哪幾種工作佇列?
  • 使用無界佇列的執行緒池會導致記憶體飆升嗎?
  • 說說幾種常見的執行緒池及使用場景?

執行緒池的建立與使用

在JDK5版本中增加了內建執行緒池實現ThreadPoolExecutor,同時提供了Executors來建立不同型別的執行緒池。Executors中提供了以下常見的執行緒池建立方法:

  • newSingleThreadExecutor:一個單執行緒的執行緒池。如果因異常結束,會再建立一個新的,保證按照提交順序執行。
  • newFixedThreadPool:建立固定大小的執行緒池。根據提交的任務逐個增加執行緒,直到最大值保持不變。如果因異常結束,會新建立一個執行緒補充。
  • newCachedThreadPool:建立一個可快取的執行緒池。會根據任務自動新增或回收執行緒。
  • newScheduledThreadPool:支援定時以及週期性執行任務的需求。
  • newWorkStealingPool:JDK8新增,根據所需的並行層次來動態建立和關閉執行緒,通過使用多個佇列減少競爭,底層使用ForkJoinPool來實現。優勢在於可以充分利用多CPU,把一個任務拆分成多個“小任務”,放到多個處理器核心上並行執行;當多個“小任務”執行完成之後,再將這些執行結果合併起來即可。

雖然在JDK中提供Executors類來支援以上型別的執行緒池建立,但通常情況下不建議開發人員直接使用(見《阿里巴巴java開發規範》)。

執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。

Executors部分方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor主要問題是堆積的請求處理佇列可能會耗費非常大的記憶體,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool:主要問題是執行緒數最大數是Integer.MAX_VALUE,可能會建立數量非常多的執行緒,甚至OOM。

同時,阿里巴巴java開發規範中推薦了3種執行緒池建立方式。

方式一,引入commons-lang3包。

//org.apache.commons.lang3.concurrent.BasicThreadFactory
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
    new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());複製程式碼

方式二,引入com.google.guava包。

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
    .setNameFormat("demo-pool-%d").build();

//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

pool.execute(()-> System.out.println(Thread.currentThread().getName()));
pool.shutdown();//gracefully shutdown複製程式碼

方式三,spring配置執行緒池方式:自定義執行緒工廠bean需要實現ThreadFactory,可參考該介面的其它預設實現類,使用方式直接注入bean,呼叫execute(Runnable task)方法即可。

<bean id="userThreadPool"
    class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="10" />
    <property name="maxPoolSize" value="100" />
    <property name="queueCapacity" value="2000" />

<property name="threadFactory" value= threadFactory />
    <property name="rejectedExecutionHandler">
        <ref local="rejectedExecutionHandler" />
    </property>
</bean>
// in code
userThreadPool.execute(thread);複製程式碼

ThreadPoolExecutor的構造方法

除了以上推薦的建立執行緒池的方法,還可以通過ThreadPoolExecutor的構造方法,直接建立執行緒池。本質上來講,以上方法最終也是建立了ThreadPoolExecutor物件,然後堆積進行包裝處理。

ThreadPoolExecutor提供了多個構造方法,我們最終都呼叫的構造方法來進行說明。

 public ThreadPoolExecutor(int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory,
      RejectedExecutionHandler handler) {
   // 省略程式碼
}複製程式碼

核心引數作用解析如下:

  • corePoolSize:執行緒池核心執行緒數最大值。
  • maximumPoolSize:執行緒池最大執行緒數大小。
  • keepAliveTime:執行緒池中非核心執行緒空閒的存活時間大小。
  • unit:執行緒空閒存活時間單位。
  • workQueue:存放任務的阻塞佇列。
  • threadFactory:建立新執行緒的工廠,所有執行緒都是通過該工廠建立的,有預設實現。
  • handler:執行緒池的拒絕策略。

程池的拒絕策略

構造方法的中最後的引數RejectedExecutionHandler用於指定執行緒池的拒絕策略。當請求任務不斷的過來,而系統此時又處理不過來的時候,我們就需要採取對應的策略是拒絕服務。

預設有四種型別:

  • AbortPolicy策略:該策略會直接丟擲異常,阻止系統正常工作。
  • CallerRunsPolicy策略:只要執行緒池未關閉,該策略直接在呼叫者執行緒中,執行當前的被丟棄的任務。
  • DiscardOleddestPolicy策略:該策略將丟棄最老的一個請求,也就是即將被執行的任務,並嘗試再次提交當前任務。
  • DiscardPolicy策略:該策略默默的丟棄無法處理的任務,不予任何處理。

當然,除了預設的4種策略之外,還可以根據業務需求自定義拒絕策略。通過實現RejectedExecutionHandler介面,在建立ThreadPoolExecutor物件時作為引數傳入即可。

在spring-integration-core中便自定義了CallerBlocksPolicy,相關程式碼如下:

public class CallerBlocksPolicy implements RejectedExecutionHandler {
    private static final Log logger = LogFactory.getLog(CallerBlocksPolicy.class);
    private final long maxWait;

    public CallerBlocksPolicy(long maxWait) {
        this.maxWait = maxWait;
    }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (!executor.isShutdown()) {
            try {
                BlockingQueue<Runnable> queue = executor.getQueue();
                if (logger.isDebugEnabled()) {
                    logger.debug("Attempting to queue task execution for " + this.maxWait + " milliseconds");
                }

                if (!queue.offer(r, this.maxWait, TimeUnit.MILLISECONDS)) {
                    throw new RejectedExecutionException("Max wait time expired to queue task");
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Task execution queued");
                    }

                }
            } catch (InterruptedException var4) {
                Thread.currentThread().interrupt();
                throw new RejectedExecutionException("Interrupted", var4);
            }
        } else {
            throw new RejectedExecutionException("Executor has been shut down");
        }
    }
}複製程式碼

執行緒池的執行

建立完成ThreadPoolExecutor之後,當向執行緒池提交任務時,通常使用execute方法。execute方法的執行流程圖如下:

image

  • 如果執行緒池中存活的核心執行緒數小於執行緒數corePoolSize時,執行緒池會建立一個核心執行緒去處理提交的任務。
  • 如果執行緒池核心執行緒數已滿,即執行緒數已經等於corePoolSize,一個新提交的任務,會被放進任務佇列workQueue排隊等待執行。
  • 當執行緒池裡面存活的執行緒數已經等於corePoolSize了,並且任務佇列workQueue也滿,判斷執行緒數是否達到maximumPoolSize,即最大執行緒數是否已滿,如果沒到達,建立一個非核心執行緒執行提交的任務。
  • 如果當前的執行緒數達到了maximumPoolSize,還有新的任務過來的話,直接採用拒絕策略處理。

原始碼分析

下面看一下JDK8中ThreadPoolExecutor中execute方法的原始碼實現:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // 執行緒池本身的狀態跟worker數量使用同一個變數ctl來維護
    int c = ctl.get();
    // 通過位運算得出當然執行緒池中的worker數量與構造引數corePoolSize進行比較
    if (workerCountOf(c) < corePoolSize) {
        // 如果小於corePoolSize,則直接新增一個worker,並把當然使用者提交的任務command作為引數,如果成功則返回。
        if (addWorker(command, true))
            return;
        // 如果失敗,則獲取最新的執行緒池資料
        c = ctl.get();
    }
    // 如果執行緒池仍在執行,則把任務放到阻塞佇列中等待執行。
    if (isRunning(c) && workQueue.offer(command)) {
        // 這裡的recheck思路是為了處理併發問題
        int recheck = ctl.get();
        // 當任務成功放入佇列時,如果recheck發現執行緒池已經不再執行了則從佇列中把任務刪除
        if (! isRunning(recheck) && remove(command))
            //刪除成功以後,會呼叫構造引數傳入的拒絕策略。
            reject(command);
         // 如果worker的數量為0(此時佇列中可能有任務沒有執行),則新建一個worker(由於此時新建woker的目的是執行佇列中堆積的任務,
         // 因此入參沒有執行任務,詳細邏輯後面會詳細分析addWorker方法)。
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果前面的新增woker,放入佇列都失敗,則會繼續新增worker,此時執行緒池的狀態是woker數量達到corePoolSize,阻塞佇列任務已滿
    // 只能基於maximumPoolSize引數新建woker
    else if (!addWorker(command, false))
        // 如果基於maximumPoolSize新建woker失敗,此時是執行緒池中執行緒數已達到上限,佇列已滿,則呼叫構造引數中傳入的拒絕策略
        reject(command);
}複製程式碼

下面再看在上述程式碼中呼叫的addWorker方法的原始碼實現及解析:

private boolean addWorker(Runnable firstTask, boolean core) {
    // 這裡有一段基於CAS+死迴圈實現的關於執行緒池狀態,執行緒數量的校驗與更新邏輯就先忽略了,重點看主流程。
    //...

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
         // 把指定任務作為引數新建一個worker執行緒
        w = new Worker(firstTask);
        // 這裡是重點w.thread是通過執行緒池建構函式引數threadFactory生成的woker物件
        // 也就是說這個變數t就是代表woker執行緒。絕對不是使用者提交的執行緒任務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()) 
                        throw new IllegalThreadStateException();
                    // 把新建的woker執行緒放入集合儲存,這裡使用的是HashSet
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 然後啟動woker執行緒
                 // 該變數t代表woker執行緒,會呼叫woker的run方法
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            // 如果woker啟動失敗,則進行一些善後工作,比如說修改當前woker數量等
            addWorkerFailed(w);
    }
    return workerStarted;
}複製程式碼

addWorker方法主要做的工作就是新建一個Woker執行緒,加入到woker集合中。在上述方法中會呼叫到Worker類的run方法,並最終執行了runWorker方法。

// Woker類實現了Runnable介面
public void run() {
    runWorker(this);
}

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // task就是Woker建構函式入參指定的任務,即使用者提交的任務
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); 
    boolean completedAbruptly = true;
    try {
        //一般情況下,task都不會為空(特殊情況上面註釋中也說明了),因此會直接進入迴圈體中
        //這裡getTask方法是要重點說明的,它的實現跟我們構造引數設定存活時間有關
        //我們都知道構造引數設定的時間代表了執行緒池中的執行緒,即woker執行緒的存活時間,如果到期則回收woker執行緒,這個邏輯的實現就在getTask中。
        //來不及執行的任務,執行緒池會放入一個阻塞佇列,getTask方法就是去阻塞佇列中取任務,使用者設定的存活時間,就是
        //從這個阻塞佇列中取任務等待的最大時間,如果getTask返回null,意思就是woker等待了指定時間仍然沒有
        //取到任務,此時就會跳過迴圈體,進入woker執行緒的銷燬邏輯。
        while (task != null || (task = getTask()) != null) {
            w.lock();
            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 {
                //這裡設為null,也就是迴圈體再執行的時候會呼叫getTask方法
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //當指定任務執行完成,阻塞佇列中也取不到可執行任務時,會進入這裡,做一些善後工作,比如在corePoolSize跟maximumPoolSize之間的woker會進行回收
        processWorkerExit(w, completedAbruptly);
    }
}複製程式碼

woker執行緒的執行流程就是首先執行初始化時分配給的任務,執行完成以後會嘗試從阻塞佇列中獲取可執行的任務,如果指定時間內仍然沒有任務可以執行,則進入銷燬邏輯。這裡只會回收corePoolSize與maximumPoolSize直接的那部分woker。

execute與submit的區別

執行任務除了可以使用execute方法還可以使用submit方法。它們的主要區別是:execute適用於不需要關注返回值的場景,submit方法適用於需要關注返回值的場景。

異常處理

當執行任務時發生異常,那麼該怎麼處理呢?首先看當Thread執行緒異常如何處理。

在任務中通過try...catch是可以捕獲異常並進行處理的,如下程式碼:

Thread t = new Thread(() -> {
    try {
        System.out.println(1 / 0);
    } catch (Exception e) {
        LOGGER.error(e.getMessage(), e);
    }
});
t.start();複製程式碼

如果很多執行緒任務預設的異常處理機制都是相同的,可以通過Thread類的UncaughtExceptionHandler來設定執行緒預設的異常處理機制。

實現UncaughtExceptionHandler介面,並呼叫Thread#setUncaughtExceptionHandler(UncaughtExceptionHandler)方法。如果想設定為全域性預設異常處理機制,則可呼叫Thread#setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)方法。

ThreadGroup預設提供了異常處理機制如下:

public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } else if (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \""
                             + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}複製程式碼

ThreadPoolExecutor的異常處理機制與Thread是一樣的。同時,ThreadPoolExecutor提供了uncaughtExceptionHandler方法來設定異常處理。如下示例:

public class ThreadPool {

    public static void main(String[] args) {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("demo-pool-%d")
                .setUncaughtExceptionHandler(new LogUncaughtExceptionHandler())
                .build();

        ExecutorService pool = new ThreadPoolExecutor(5, 200,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

        pool.execute(() -> {
            throw new RuntimeException("測試異常");
        });

        pool.shutdown();
    }

    static class  LogUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("列印LogUncaughtExceptionHandler中獲得的異常資訊:" + e.getMessage());
        }
    }
}複製程式碼

但需要注意的是使用UncaughtExceptionHandler的方法只適用於execute方法執行的任務,而對submit方法是無效。submit執行的任務,可以通過返回的Future物件的get方法接收丟擲的異常,再進行處理。這也算是execute方法與submit方法的差別之一。

執行緒池中常見的佇列

執行緒池有以下工作佇列:

  • ArrayBlockingQueue:有界佇列,是一個用陣列實現的有界阻塞佇列,按FIFO排序量。
  • LinkedBlockingQueue:可設定容量佇列,基於連結串列結構的阻塞佇列,按FIFO排序任務,容量可以選擇進行設定,不設定的話,將是一個無邊界的阻塞佇列,最大長度為Integer.MAX_VALUE,吞吐量通常要高於ArrayBlockingQuene;newFixedThreadPool執行緒池使用了這個佇列。
  • DelayQueue:延遲佇列,是一個任務定時週期的延遲執行的佇列。根據指定的執行時間從小到大排序,否則根據插入到佇列的先後排序。newScheduledThreadPool執行緒池使用了這個佇列。
  • PriorityBlockingQueue:優先順序佇列,是具有優先順序的無界阻塞佇列。
  • SynchronousQueue:同步佇列,一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQuene,newCachedThreadPool執行緒池使用了這個佇列。

關閉執行緒池

關閉執行緒池可以呼叫shutdownNow和shutdown兩個方法來實現。

shutdownNow:對正在執行的任務全部發出interrupt(),停止執行,對還未開始執行的任務全部取消,並且返回還沒開始的任務列表。

shutdown:當我們呼叫shutdown後,執行緒池將不再接受新的任務,但也不會去強制終止已經提交或者正在執行中的任務。

參考文章:

https://www.jianshu.com/p/5df6e38e4362

https://juejin.im/post/5d1882b1f265da1ba84aa676

原文連結:《面試題-關於Java執行緒池一篇文章就夠了


程式新視界:精彩和成長都不容錯過

程式新視界-微信公眾號

相關文章