面試官一個線程池問題把我問懵逼了。

這是why的第 98 篇原創文章java

前幾天,有個朋友在微信上找我。他問:why哥,在嗎?node

我說:發生腎麼事了?面試

他啪的一下就提了一個問題啊,很快。編程

我大意了,隨意瞅了一眼,這題不是很簡單嗎?微信

結果沒想到裏面還隱藏着一篇文章。併發

故事,得從這個問題提及:ide

上面的圖中的線程池配置是這樣的:spa

ExecutorService executorService = new ThreadPoolExecutor(40, 80, 1, TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(100), 
                new DefaultThreadFactory("test"),
                new ThreadPoolExecutor.DiscardPolicy());

上面這個線程池裏面的參數、執行流程啥的我就再也不解釋了。操作系統

畢竟我曾經在《一人血書,想讓why哥講一下這道面試題。》這篇文章裏面發過毒誓的,再說就是小王吧了:線程

上面的這個問題其實就是一個很是簡單的八股文問題:

非核心線程在何時被回收?

若是通過 keepAliveTime 時間後,超過核心線程數的線程尚未接受到新的任務,就會被回收。

標準答案,徹底沒毛病。

那麼我如今帶入一個簡單的場景,爲了簡單直觀,咱們把線程池相關的參數調整一下:

ExecutorService executorService = new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2), 
                new DefaultThreadFactory("test"),
                new ThreadPoolExecutor.DiscardPolicy());

那麼問題來了:

  • 這個線程最多能容納的任務是否是 5 個?
  • 假設任務須要執行 1 秒鐘,那麼我直接循環裏面提交 5 個任務到線程池,確定是在 1 秒鐘以內提交完成,那麼當前線程池的活躍線程是否是就是 3 個?
  • 若是接下來的 30 秒,沒有任務提交過來。那麼 30 秒以後,當前線程池的活躍線程是否是就是 2 個?

上面這三個問題的答案都是確定的,若是你搞不明白爲何,那麼我建議你先趕忙去補充一下線程池相關的知識點,下面的內容你強行看下去確定是一臉懵逼的。

接下來的問題是這樣的:

  • 若是當前線程池的活躍線程是 3 個(2 個核心線程+ 1 個非核心線程),可是它們各自的任務都執行完成了,都處於 waiting 狀態。而後我每隔 3 秒往線程池裏面扔一個耗時 1 秒的任務。那麼 30 秒以後,活躍線程數是多少?

先說答案:仍是 3 個。

從我我的正常的思惟,是這樣的:核心線程是空閒的,每隔 3 秒扔一個耗時 1 秒的任務過來,因此僅須要一個核心線程就徹底處理的過來。

那麼,30 秒內,超過核心線程的那一個線程一直處於等待狀態,因此​ 30 秒以後,就被回收了。
可是上面僅僅是個人主觀認爲,而實際狀況呢?

30 秒以後,超過核心線程​的線程並不會被回收,活躍線程仍是 3 個。
到這裏,若是你知道是 3 個,且知道爲何是 3 個,即瞭解爲何非核心線程並無被回收,那麼接下里的內容應該就是你已經掌握的了。

能夠不看,拉到最後,點個贊,去忙本身的事情吧。

若是你不知道,能夠接着看,瞭解一下爲何是 3 個。

雖然我相信沒有面試官會問這樣的問題,可是對於你去理解線程池,是有幫助的。

先上 Demo

基於我前面說的這個場景,碼出代碼以下:

public class ThreadTest {

    @Test
    public void test() throws InterruptedException {

        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2), new DefaultThreadFactory("test"),
                new ThreadPoolExecutor.DiscardPolicy());
                
        //每隔兩秒打印線程池的信息
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("=====================================thread-pool-info:" + new Date() + "=====================================");
            System.out.println("CorePoolSize:" + executorService.getCorePoolSize());
            System.out.println("PoolSize:" + executorService.getPoolSize());
            System.out.println("ActiveCount:" + executorService.getActiveCount());
            System.out.println("KeepAliveTime:" + executorService.getKeepAliveTime(TimeUnit.SECONDS));
            System.out.println("QueueSize:" + executorService.getQueue().size());
        }, 0, 2, TimeUnit.SECONDS);

        try {
            //同時提交5個任務,模擬達到最大線程數
            for (int i = 0; i < 5; i++) {
                executorService.execute(new Task());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //休眠10秒,打印日誌,觀察線程池狀態
        Thread.sleep(10000);

        //每隔3秒提交一個任務
        while (true) {
            Thread.sleep(3000);
            executorService.submit(new Task());
        }
    }

    static class Task implements Runnable {
        @Override
        public void run(){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "-執行任務");
        }
    }
}

這份代碼也是提問的哥們給個人,我作了微調,你直接粘出去就能跑起來。

show me code,no bb。這纔是相互探討的正確姿式。

這個程序的運行結果是這樣的:

一共五個任務,線程池的運行狀況是什麼樣的呢?

先看標號爲 ① 的地方:

三個線程都在執行任務,而後 2 號線程和 1 號線程率先完成了任務,接着把隊列裏面的兩個任務拿出來執行(標號爲 ② 的地方)。

按照程序,接下來,每隔 3 秒就有一個耗時 1 秒的任務過來。而此時線程池裏面的三個活躍線程都是空閒狀態。

那麼問題就來了:

該選擇哪一個線程來執行這個任務呢?是隨機選一個嗎?

雖然接下來的程序尚未執行,可是基於前面的截圖,我如今就能夠告訴你,接下來的任務,線程執行順序爲:

  • Thread[test-1-3,5,main]-執行任務
  • Thread[test-1-2,5,main]-執行任務
  • Thread[test-1-1,5,main]-執行任務
  • Thread[test-1-3,5,main]-執行任務
  • Thread[test-1-2,5,main]-執行任務
  • Thread[test-1-1,5,main]-執行任務
  • ......

即雖然線程都是空閒的,可是當任務來的時候不是隨機調用的,而是輪詢。

因爲是輪詢,每三秒執行一次,因此非核心線程的空閒時間最多也就是 9 秒,不會超過 30 秒,因此一直不會被回收。

基於這個 Demo,咱們就從表象上回答了,爲何活躍線程數一直爲 3。

爲何是輪詢?

咱們經過 Demo 驗證了上面場景中,線程執行順序爲輪詢。

那麼爲何呢?

這只是經過日誌得出的表象呀,內部原理呢?對應的代碼呢?

這一小節帶你們看一下究竟是怎麼回事。

首先我看到這個表象的時候我就猜想:這三個線程確定是在某個地方被某個隊列存起來了,基於此,才能實現輪詢調用。

因此,我一直在找這個隊列,一直沒有找到對應的代碼,我還有點着急了。想着不會是在操做系統層面控制的吧?

後來我冷靜下來,以爲不太可能。因而電光火石之間,我想到了,要不先 Dump 一下線程,看看它們都在幹啥:

Dump 以後,這玩意我眼熟啊,AQS 的等待隊列啊。

根據堆棧信息,咱們能夠定位到這裏的源碼:

java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#awaitNanos

看到這裏的時候,我才一下恍然大悟了起來。

害,是本身想的太多了。

說穿了,這其實就是個生產者-消費者的問題啊。

三個線程就是三個消費者,如今沒有任務須要處理,它們就等着生產者生產任務,而後通知它們準備消費。

因爲本文只是帶着你去找答案在源碼的什麼地方,不對源碼進行解讀。

因此我默認你是對 AQS 是有必定的瞭解的。

能夠看到 addConditionWaiter 方法其實就是在操做咱們要找的那個隊列。學名叫作等待隊列。

Debug 一下,看看隊列裏面的狀況:

巧了嘛,這不是。順序恰好是:

  • Thread[test-1-3,5,main]
  • Thread[test-1-2,5,main]
  • Thread[test-1-1,5,main]

消費者這邊咱們大概摸清楚了,接着去看看生產者。

  • java.util.concurrent.ThreadPoolExecutor#execute

線程池是在這裏把任務放到隊列裏面去的。

而這個方法裏面的源碼是這樣的:

其中signalNotEmpty() 最終會走到 doSignal 方法,而該方法裏面會調用 transferForSignal 方法。

這個方法裏面會調用 LockSupport.unpark(node.thred) 方法,喚醒線程:

而喚醒的順序,就是等待隊列裏面的順序:

因此,如今你知道當一個任務來了以後,這個任務該由線程池裏面的哪一個線程執行,這個不是隨機的,也不是隨便來的。

是講究一個順序的。

什麼順序呢?

Condition 裏面的等待隊列裏面的順序。

什麼,你不太懂 Condition?

那還不趕忙去學?等着我給你講呢?

原本我是想寫一下的,後來發現《Java併發編程的藝術》一書中的 5.6.2 小節已經寫的挺清楚了,圖文並茂。這部份內容其實也是面試的時候的高頻考點,因此本身去看看就行了。

先欠着,欠着。

非核心線程怎麼回收?

仍是上面的例子,假設非核心線程就空閒了超過 30 秒,那麼它是怎麼被回收的呢?

這個也是一個比較熱門的面試題。

這題沒有什麼高深的地方,答案就藏在源碼的這個地方:

  • java.util.concurrent.ThreadPoolExecutor#getTask

當 timed 參數爲 true 的時候,會執行 workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) 方法。

而 timed 何時爲 true 呢?

  • boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

allowCoreThreadTimeOut 默認爲 false。

因此,就是看 wc > corePoolSize 條件,wc 是活躍線程數。此時活躍線程數爲 3 ,大於核心線程數 2。

所以 timed 爲 true。

也就是說,當前 workQueue 爲空的時候,如今三個線程都阻塞 workQueue.poll 方法中。

而當指定時間後,workQueue 仍是爲空,則返回爲 null。

因而在 1077 行把 timeOut 修改成 true。

進入一下次循環,返回 null。

最終會執行到這個方法:

  • java.util.concurrent.ThreadPoolExecutor#processWorkerExit

而這個方法裏面會執行 remove 的操做。

因而線程就被回收了。

因此當超過指定時間後,線程會被回收。

那麼被回收的這個線程是核心線程仍是非核心線程呢?

不知道。

由於在線程池裏面,核心線程和非核心線程僅僅是一個概念而已,其實拿着一個線程,咱們並不能知道它是核心線程仍是非核心線程。

這個地方就是一個證實,由於當工做線程多餘核心線程數以後,全部的線程都在 poll,也就是說全部的線程都有可能被回收:

另一個強有力的證實就是 addWorker 這裏:

core 參數僅僅是控制取 corePoolSize 仍是 maximumPoolSize。

因此,這個問題你說怎麼回答:

JDK 區分的方式就是不區分。

那麼咱們能夠知道嗎?

能夠,好比經過觀察日誌,前面的案例中,我就知道這兩個是核心線程,由於它們最早建立:

  • Thread[test-1-1,5,main]-執行任務
  • Thread[test-1-2,5,main]-執行任務

在程序裏面怎麼知道呢?

目前是不知道的,可是這個需求,加錢就能夠實現。

本身擴展一下線程池嘛,給線程池裏面的線程打個標還不是一件很簡單的事情嗎?

只是你想一想,你區分這玩意幹啥,有沒有可落地的需求?

畢竟,脫離需求談實現。都是耍流氓。

最後說一句

才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,能夠在後臺提出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

相關文章
相關標籤/搜索