<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 10655字,需瀏覽 22分鐘

           ·

          2021-05-24 16:17


          你好呀,我是why哥。

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

          我說:發(fā)生腎么事了?

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

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

          結(jié)果沒想到里面還隱藏著一篇文章。

          故事,得從這個問題說起:

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

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

          上面這個線程池里面的參數(shù)、執(zhí)行流程啥的我就不再解釋了。

          畢竟我曾經(jīng)在《一人血書,想讓why哥講一下這道面試題。》這篇文章里面發(fā)過毒誓的,再說就是小王吧了:

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

          非核心線程在什么時候被回收?

          如果經(jīng)過 keepAliveTime 時間后,超過核心線程數(shù)的線程還沒有接受到新的任務(wù),就會被回收。

          標(biāo)準(zhǔn)答案,完全沒毛病。

          那么我現(xiàn)在帶入一個簡單的場景,為了簡單直觀,我們把線程池相關(guān)的參數(shù)調(diào)整一下:

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

          那么問題來了:

          • 這個線程最多能容納的任務(wù)是不是 5 個?
          • 假設(shè)任務(wù)需要執(zhí)行 1 秒鐘,那么我直接循環(huán)里面提交 5 個任務(wù)到線程池,肯定是在 1 秒鐘之內(nèi)提交完成,那么當(dāng)前線程池的活躍線程是不是就是 3 個?
          • 如果接下來的 30 秒,沒有任務(wù)提交過來。那么 30 秒之后,當(dāng)前線程池的活躍線程是不是就是 2 個?

          上面這三個問題的答案都是肯定的,如果你搞不明白為什么,那么我建議你先趕緊去補(bǔ)充一下線程池相關(guān)的知識點,下面的內(nèi)容你強(qiáng)行看下去肯定是一臉懵逼的。

          接下來的問題是這樣的,請聽題:

          • 如果當(dāng)前線程池的活躍線程是 3 個(2 個核心線程+ 1 個非核心線程),但是它們各自的任務(wù)都執(zhí)行完成了。然后我每隔 3 秒往線程池里面扔一個耗時 1 秒的任務(wù)。那么 30 秒之后,活躍線程數(shù)是多少?

          先說答案:還是 3 個。

          從我個人正常的思維,是這樣的:核心線程是空閑的,每隔 3 秒扔一個耗時 1 秒的任務(wù)過來,所以僅需要一個核心線程就完全處理的過來。

          那么,30 秒內(nèi),超過核心線程的那一個線程一直處于等待狀態(tài),所以 30 秒之后,就被回收了。

          但是上面僅僅是我的主觀認(rèn)為,而實際情況呢?

          30 秒之后,超過核心線程的線程并不會被回收,活躍線程還是 3 個。

          到這里,如果你知道是 3 個,且知道為什么是 3 個,即了解為什么非核心線程并沒有被回收,那么接下里的內(nèi)容應(yīng)該就是你已經(jīng)掌握的了。

          可以不看,拉到最后,點個贊,去忙自己的事情吧。

          如果你不知道,可以接著看,了解一下為什么是 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個任務(wù),模擬達(dá)到最大線程數(shù)
                      for (int i = 0; i < 5; i++) {
                          executorService.execute(new Task());
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
                  //休眠10秒,打印日志,觀察線程池狀態(tài)
                  Thread.sleep(10000);

                  //每隔3秒提交一個任務(wù)
                  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() + "-執(zhí)行任務(wù)");
                  }
              }
          }

          這份代碼也是提問的哥們給我的,我做了細(xì)微的調(diào)整,你直接粘出去就能跑起來。

          show code,no bb。這才是相互探討的正確姿勢。

          這個程序的運(yùn)行結(jié)果是這樣的:

          一共五個任務(wù),線程池的運(yùn)行情況是什么樣的呢?

          先看標(biāo)號為 ① 的地方:

          三個線程都在執(zhí)行任務(wù),然后 2 號線程和 1 號線程率先完成了任務(wù),接著把隊列里面的兩個任務(wù)拿出來執(zhí)行(標(biāo)號為 ② 的地方)。

          按照程序,接下來,每隔 3 秒就有一個耗時 1 秒的任務(wù)過來。而此時線程池里面的三個活躍線程都是空閑狀態(tài)。

          那么問題就來了:

          該選擇哪個線程來執(zhí)行這個任務(wù)呢?是隨機(jī)選一個嗎?

          雖然接下來的程序還沒有執(zhí)行,但是基于前面的截圖,我現(xiàn)在就可以告訴你,接下來的任務(wù),線程執(zhí)行順序為:

          • Thread[test-1-3,5,main]-執(zhí)行任務(wù)
          • Thread[test-1-2,5,main]-執(zhí)行任務(wù)
          • Thread[test-1-1,5,main]-執(zhí)行任務(wù)
          • Thread[test-1-3,5,main]-執(zhí)行任務(wù)
          • Thread[test-1-2,5,main]-執(zhí)行任務(wù)
          • Thread[test-1-1,5,main]-執(zhí)行任務(wù)
          • ......

          即在我們的案例中,雖然線程都是空閑的,但是當(dāng)任務(wù)來的時候不是隨機(jī)調(diào)用的,而是輪詢。

          由于是輪詢,每三秒執(zhí)行一次,所以非核心線程的空閑時間最多也就是 9 秒,不會超過 30 秒,所以一直不會被回收。

          基于這個 Demo,我們就從表象上回答了,為什么活躍線程數(shù)一直為 3。

          這個地方就和我的認(rèn)知有點出入了,于是我稍微的研究了一下為什么是輪詢。

          為什么是輪詢?

          我們通過 Demo 驗證了在上面場景中線程執(zhí)行順序為輪詢。

          那么為什么呢?

          這只是通過日志得出的表象呀,內(nèi)部原理呢?對應(yīng)的代碼呢?

          這一小節(jié)帶大家看一下到底是怎么回事。

          首先我看到這個表象的時候我就猜測:這三個線程肯定是在某個地方被某個隊列存起來了,基于此,才能實現(xiàn)輪詢調(diào)用。

          所以,我一直在找這個隊列,一直沒有找到對應(yīng)的代碼,我還有點著急了。想著不會是在操作系統(tǒng)層面控制的吧?

          后來我冷靜下來,覺得不太可能。于是電光火石之間,我想到了,要不先 Dump 一下線程,看看它們都在干啥:

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

          先說明一下:由于本文只是帶著你去找答案在源碼的什么地方,不對源碼進(jìn)行解讀。所以我默認(rèn)你是對 AQS 是有一定的了解的。

          接著根據(jù)堆棧信息,我們可以定位到這里的源碼:

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

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

          害,是自己想的太多了。

          說穿了,這其實就是個生產(chǎn)者-消費(fèi)者的問題啊。

          三個線程就是三個消費(fèi)者,現(xiàn)在沒有任務(wù)需要處理,它們就等著生產(chǎn)者生產(chǎn)任務(wù),然后通知它們準(zhǔn)備消費(fèi)。

          可以看到 addConditionWaiter 方法其實就是在操作我們要找的那個隊列,學(xué)名叫做等待隊列。

          Debug 一下,看看隊列里面的情況:

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

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

          消費(fèi)者這邊我們大概摸清楚了,接著去看看生產(chǎn)者。

          • java.util.concurrent.ThreadPoolExecutor#execute

          線程池是在這里把任務(wù)放到隊列里面去的。

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

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

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

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

          所以,現(xiàn)在你知道當(dāng)一個任務(wù)來了之后,這個任務(wù)該由線程池里面的哪個線程執(zhí)行,這個不是隨機(jī)的,也不是隨便來的。

          是講究一個順序的。

          什么順序呢?

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

          什么,你不太懂 Condition?

          那還不趕緊去學(xué)?

          本來我是想寫一下的,后來發(fā)現(xiàn)《Java并發(fā)編程的藝術(shù)》一書中的 5.6.2 小節(jié)已經(jīng)寫的挺清楚了,圖文并茂。這部分內(nèi)容其實也是面試的時候的高頻考點,所以自己去看看就好了。

          我就把我寫的這部分內(nèi)容刪除了,先就不贅述了吧。

          哦,你不想看書,就想等著我給你講呢?

          先欠著,欠著。

          偷個懶,文章寫太長了也沒人看。

          非核心線程怎么回收?

          還是上面的例子,假設(shè)非核心線程就空閑了超過 30 秒,那么它是怎么被回收的呢?

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

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

          • java.util.concurrent.ThreadPoolExecutor#getTask

          當(dāng) timed 參數(shù)為 true 的時候,會執(zhí)行 workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS) 方法。

          而 timed 什么時候為 true 呢?

          • boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

          allowCoreThreadTimeOut 默認(rèn)為 false。

          所以,就是看 wc > corePoolSize 條件,wc 是活躍線程數(shù)。此時活躍線程數(shù)為 3 ,大于核心線程數(shù) 2。

          因此 timed 為 true。

          也就是說,當(dāng)前 workQueue 為空的時候,現(xiàn)在三個線程都阻塞 workQueue.poll 方法中。

          而當(dāng)指定時間后,workQueue 還是為空,則返回為 null。

          于是在 1077 行把 timeOut 修改為 true。

          進(jìn)入一下次循環(huán),返回 null。

          最終會執(zhí)行到這個方法:

          • java.util.concurrent.ThreadPoolExecutor#processWorkerExit

          而這個方法里面會執(zhí)行 remove 的操作。

          于是線程就被回收了。

          所以當(dāng)超過指定時間后,線程會被回收。

          那么被回收的這個線程是核心線程還是非核心線程呢?

          不知道。

          因為在線程池里面,核心線程和非核心線程僅僅是一個概念而已,其實拿著一個線程,我們并不能知道它是核心線程還是非核心線程。

          這個地方就是一個證明,因為當(dāng)工作線程多余核心線程數(shù)之后,所有的線程都在 poll,也就是說所有的線程都有可能被回收:

          另外一個強(qiáng)有力的證明就是 addWorker 這里:

          core 參數(shù)僅僅是控制取 corePoolSize 還是 maximumPoolSize。

          所以,這個問題你說怎么回答:

          JDK 區(qū)分的方式就是不區(qū)分。

          那么我們可以知道嗎?

          可以,比如通過觀察日志,前面的案例中,我就知道這兩個是核心線程,因為它們最先創(chuàng)建:

          • Thread[test-1-1,5,main]-執(zhí)行任務(wù)
          • Thread[test-1-2,5,main]-執(zhí)行任務(wù)

          在程序里面怎么知道呢?

          這個就比較難了,其實我覺得這個信息并不重要吧?

          什么,你加錢?

          加錢,加錢可以實現(xiàn)。

          自己擴(kuò)展一下線程池嘛,給線程池里面的線程打個標(biāo)還不是一件很簡單的事情嗎?

          只是你想想,你區(qū)分這玩意干啥,有沒有可落地的需求?

          畢竟,脫離需求談實現(xiàn)。都是耍流氓。

          荒腔走板

          周末的時候收到了之前在網(wǎng)上買的一個頭戴式藍(lán)牙耳機(jī),放在家里用。

          本來我是想多選選、多看看、多糾結(jié)一下的。

          但是當(dāng)我刷購物 APP 的時候,突然魅族的這款耳機(jī)映入了我的眼睛里面。

          在這之前,其實我完全不知道這款耳機(jī)的。然后點進(jìn)去簡單看了一下介紹,甚至都沒看評論,就直接下單了。

          不為別的,只是因為我曾經(jīng)也是魅族的忠實用戶。

          但是下單的時候我就在想:這應(yīng)該是我最后一次為魅族充值了吧。

          我用的第一款智能手機(jī)就是從同學(xué)那里買來的魅族的 M8, 10 年過去了,我至今都還記得當(dāng)時第一眼看到它的驚艷。

          一款手機(jī),怎么能做的這么漂亮呢?

          提前 M8,了解魅族的朋友、了解這款手機(jī)的朋友都會由衷的說一句:國貨之光。

          由于 M8 給我?guī)淼牧己皿w驗,后來的 10 年間,我每次換手機(jī)都是魅族。

          我真的喜歡魅族、喜歡 flyme 系統(tǒng)、喜歡 mBack。

          直到后面,止步于 Pro 7,黃章的“出山之作”。我當(dāng)時剛好換手機(jī),果斷的買了,用了。之后,我決定不再為魅族充值了。

          在我心里,魅族創(chuàng)始人黃章,才是第一個打造情懷,買情懷的人。我這句話不是貶低,而是贊揚(yáng),我曾經(jīng)就愿意為這個男人的情懷買單。

          但是,不管怎么樣,我還是希望黃章早日做出自己的“夢想機(jī)”,魅族重回巔峰。

          畢竟,我看到耳機(jī)快遞里面魅族科技的卡片時,內(nèi)心還是有一絲波瀾的。

          畢竟,我曾經(jīng)也是魅友。

          說到這個耳機(jī),是真不錯。

          我寫文章的時候就帶著這個耳機(jī),不愧是 MP3 發(fā)家的魅族:

          高音準(zhǔn),中音甜,低音勁,總之一句話,就是通透!

          最后說一句

          好了,看到了這里安排個“一鍵三連”(轉(zhuǎn)發(fā)、在看、點贊)吧,周更很累的,需要一點正反饋。

          才疏學(xué)淺,難免會有紕漏,如果你發(fā)現(xiàn)了錯誤的地方,可以在后臺提出來,我對其加以修改。

          感謝您的閱讀,我堅持原創(chuàng),十分歡迎并感謝您的關(guān)注。

          最后,大家覺得文章還行,可以給我的號標(biāo)個星。如果不標(biāo)星,按照微信的推送機(jī)制,后續(xù)有可能會看不到我的文章。

          之前把我標(biāo)星了的讀者,你也可以看一下,遷移之后應(yīng)該神不知鬼不覺的被官方取消掉了,也需要重新進(jìn)行標(biāo)星。

          — 【 THE END 】—
          本公眾號全部博文已整理成一個目錄,請在公眾號里回復(fù)「m」獲取!

          最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。

          獲取方式:點“在看”,關(guān)注公眾號并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

          文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 76
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉肏屄 | 国产成人三级三级三级78 | 123好逼网 | 就爱操逼网 | 国产高清无码黄 |