<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>

          如圖兩道面試題,順便深入線程池,并連環(huán)17問

          共 12003字,需瀏覽 25分鐘

           ·

          2021-06-16 12:05

          你好,我是yes。

          這兩面試題是基友朋友最近去面滴滴遇到的,今天就借著這兩面試真題來深入一波線程池吧,這篇文章力求把線程池核心點和常問的面試點一網(wǎng)打盡,當(dāng)然個人能力有限,可能會有遺漏,歡迎留言補充!

          先把問題列出來,如果你都答得出來,那沒必要看下去:

          • 為什么會有線程池?
          • 簡單手寫一個線程池?
          • 為什么要把任務(wù)先放在任務(wù)隊列里面,而不是把線程先拉滿到最大線程數(shù)?
          • 線程池如何動態(tài)修改核心線程數(shù)和最大線程數(shù)?
          • 如果你是 JDK 設(shè)計者,如何設(shè)計?
          • 如果要讓你設(shè)計一個線程池,你要怎么設(shè)計?
          • 你是如何理解核心線程的?
          • 你是怎么理解 KeepAliveTime 的?
          • 那 workQueue 有什么用?
          • 你是如何理解拒絕策略的?
          • 你說你看過源碼,那你肯定知道線程池里的 ctl 是干嘛的咯?
          • 你知道線程池有幾種狀態(tài)嗎?
          • 你知道線程池的狀態(tài)是如何變遷的嗎?
          • 如何修改原生線程池,使得可以先拉滿線程數(shù)再入任務(wù)隊列排隊?
          • Tomcat 中的定制化線程池實現(xiàn) 如果線程池中的線程在執(zhí)行任務(wù)的時候,拋異常了,會怎么樣?
          • 原生線程池的核心線程一定伴隨著任務(wù)慢慢創(chuàng)建的嗎?
          • 線程池的核心線程在空閑的時候一定不會被回收嗎?

          接得住嗎?話不多說,發(fā)車!

          為什么會有線程池?

          想要深入理解線程池的原理得先知道為什么需要線程池。

          首先你要明白,線程是一個重資源,JVM 中的線程與操作系統(tǒng)的線程是一對一的關(guān)系,所以在 JVM 中每創(chuàng)建一個線程就需要調(diào)用操作系統(tǒng)提供的 API 創(chuàng)建線程,賦予資源,并且銷毀線程同樣也需要系統(tǒng)調(diào)用。

          而系統(tǒng)調(diào)用就意味著上下文切換等開銷,并且線程也是需要占用內(nèi)存的,而內(nèi)存也是珍貴的資源。

          因此線程的創(chuàng)建和銷毀是一個重操作,并且線程本身也占用資源。

          然后你還需要知道,線程數(shù)并不是越多越好。

          我們都知道線程是 CPU 調(diào)度的最小單位,在單核時代,如果是純運算的操作是不需要多線程的,一個線程一直執(zhí)行運算即可。但如果這個線程正在等待 I/O 操作,此時 CPU 就處于空閑狀態(tài),這就浪費了 CPU 的算力,因此有了多線程,在某線程等待 I/O 等操作的時候,另一個線程頂上,充分利用 CPU,提高處理效率。

          此時的多線程主要是為了提高 CPU 的利用率而提出。

          而隨著 CPU 的發(fā)展,核心數(shù)越來越多,能同時運行的線程數(shù)也提升了,此時的多線程不僅是為了提高單核 CPU 的利用率,也是為了充分利用多個核心。

          至此想必應(yīng)該明白了為什么會有多線程,無非就是為了充分利用 CPU 空閑的時間,一刻也不想讓他停下來。

          但 CPU 的核心數(shù)有限,同時能運行的線程數(shù)有限,所以需要根據(jù)調(diào)度算法切換執(zhí)行的線程,而線程的切換需要開銷,比如替換寄存器的內(nèi)容、高速緩存的失效等等。

          如果線程數(shù)太多,切換的頻率就變高,可能使得多線程帶來的好處抵不過線程切換帶來的開銷,得不償失。

          因此線程的數(shù)量需要得以控制,結(jié)合上述的描述可知,線程的數(shù)量與 CPU 核心數(shù)和 I/O 等待時長息息相關(guān)。

          小結(jié)一下:

          • Java中線程與操作系統(tǒng)線程是一比一的關(guān)系。
          • 線程的創(chuàng)建和銷毀是一個“較重”的操作。
          • 多線程的主要是為了提高 CPU 的利用率。
          • 線程的切換有開銷,線程數(shù)的多少需要結(jié)合 CPU核心數(shù)與 I/O 等待占比。

          綜上我們知道了線程的這些特性,所以說它不是一個可以“隨意拿捏”的東西,我們需要重視它,好好規(guī)劃和管理它,充分利用硬件的能力,從而提升程序執(zhí)行效率,所以線程池應(yīng)運而生。

          什么是線程池?

          那我們要如何管理好線程呢?

          因為線程數(shù)太少無法充分利用 CPU ,太多的話由于上下文切換的消耗又得不償失,所以我們需要評估系統(tǒng)所要承載的并發(fā)量和所執(zhí)行任務(wù)的特性,得出大致需要多少個線程數(shù)才能充分利用 CPU,因此需要控制線程數(shù)量

          又因為線程的創(chuàng)建和銷毀是一個“重”操作,所以我們需要避免線程頻繁地創(chuàng)建與銷毀,因此我們需要緩存一批線程,讓它們時刻準(zhǔn)備著執(zhí)行任務(wù)。

          目標(biāo)已經(jīng)很清晰了,弄一個池子,里面存放約定數(shù)量的線程,這就是線程池,一種池化技術(shù)。

          熟悉對象池、連接池的朋友肯定對池化技術(shù)不陌生,一般池化技術(shù)的使用方式是從池子里拿出資源,然后使用,用完了之后歸還。

          但是線程池的實現(xiàn)不太一樣,不是說我們從線程池里面拿一個線程來執(zhí)行任務(wù),等任務(wù)執(zhí)行完了之后再歸還線程,你可以想一下這樣做是否合理。

          線程池的常見實現(xiàn)更像是一個黑盒存在,我們設(shè)置好線程池的大小之后,直接往線程池里面丟任務(wù),然后就不管了。

          剝開來看,線程池其實是一個典型的生產(chǎn)者-消費者模式。

          線程池內(nèi)部會有一個隊列來存儲我們提交的任務(wù),而內(nèi)部線程不斷地從隊列中索取任務(wù)來執(zhí)行,這就是線程池最原始的執(zhí)行機制。

          按照這個思路,我們可以很容易的實現(xiàn)一個簡單版線程池,想必看了下面這個代碼實現(xiàn),對線程池的核心原理就會了然于心。

          首先線程池內(nèi)需要定義兩個成員變量,分別是阻塞隊列和線程列表,然后自定義線程使它的任務(wù)就是不斷的從阻塞隊列中拿任務(wù)然后執(zhí)行。

          @Slf4j
          public class YesThreadPool {

           BlockingQueue<Runnable> taskQueue;  //存放任務(wù)的阻塞隊列
           List<YesThread> threads; //線程列表

           YesThreadPool(BlockingQueue<Runnable> taskQueue, int threadSize) {
            this.taskQueue = taskQueue;
            threads = new ArrayList<>(threadSize);
            // 初始化線程,并定義名稱
            IntStream.rangeClosed(1, threadSize).forEach((i)-> {
             YesThread thread = new YesThread("yes-task-thread-" + i);
             thread.start();
             threads.add(thread);
            });
           }
           //提交任務(wù)只是往任務(wù)隊列里面塞任務(wù)
           public void execute(Runnable task) throws InterruptedException {
            taskQueue.put(task);
           }

           class YesThread extends Thread //自定義一個線程
            public YesThread(String name) {
             super(name);
            }
            @Override
            public void run() {
             while (true) { //死循環(huán)
              Runnable task = null;
              try {
               task = taskQueue.take(); //不斷從任務(wù)隊列獲取任務(wù)
              } catch (InterruptedException e) {
               logger.error("記錄點東西.....", e);
              }
              task.run(); //執(zhí)行
             }
            }
           }
          }

          一個簡單版線程池就完成了,簡單吧!

          再寫個 main 方法用一用,絲滑,非常絲滑。

           public static void main(String[] args) {
            YesThreadPool pool = new YesThreadPool(new LinkedBlockingQueue<>(10), 3);
            IntStream.rangeClosed(15).forEach((i)-> {
             try {
              pool.execute(()-> {
               System.out.println(Thread.currentThread().getName() + " 公眾號:yes的練級攻略");
              });
             } catch (InterruptedException e) {
              logger.error("記錄點東西.....", e);
             }
            });
           }

          運行結(jié)果如下:

          下次面試官讓你手寫線程池,直接上這個簡單版,然后他會開始讓你優(yōu)化,比如什么線程一開始都 start 了不好,想懶加載,然后xxxx...最終其實就是想往李老爺實現(xiàn)的 ThreadPoolExecutor 上面靠。

          那就來嘛。

          ThreadPoolExecutor 剖析

          這玩意就是常被問的線程池的實現(xiàn)類了,先來看下構(gòu)造函數(shù):

          核心原理其實和咱們上面實現(xiàn)的差不多,只是生產(chǎn)級別的那肯定是要考慮的更多,接下來我們就來看看此線程池的工作原理。

          先來一張圖:

          簡單來說線程池把任務(wù)的提交和任務(wù)的執(zhí)行剝離開來,當(dāng)一個任務(wù)被提交到線程池之后:

          • 如果此時線程數(shù)小于核心線程數(shù),那么就會新起一個線程來執(zhí)行當(dāng)前的任務(wù)。
          • 如果此時線程數(shù)大于核心線程數(shù),那么就會將任務(wù)塞入阻塞隊列中,等待被執(zhí)行。
          • 如果阻塞隊列滿了,并且此時線程數(shù)小于最大線程數(shù),那么會創(chuàng)建新線程來執(zhí)行當(dāng)前任務(wù)。
          • 如果阻塞隊列滿了,并且此時線程數(shù)大于最大線程數(shù),那么會采取拒絕策略。

          以上就是任務(wù)提交給線程池后各種狀況匯總,一個很容易出現(xiàn)理解錯誤的地方就是當(dāng)線程數(shù)達到核心數(shù)的時候,任務(wù)是先入隊,而不是先創(chuàng)建最大線程數(shù)。

          從上述可知,線程池里的線程不是一開始就直接拉滿的,是根據(jù)任務(wù)量開始慢慢增多的,這就算一種懶加載,到用的時候再創(chuàng)建線程,節(jié)省資源。

          來先吃我?guī)讍枴?/p>

          此時線程數(shù)小于核心線程數(shù),并且線程都處于空閑狀態(tài),現(xiàn)提交一個任務(wù),是新起一個線程還是給之前創(chuàng)建的線程?

          李老是這樣說的:If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task.

          我覺得把 threads are running 去了,更合理一些,此時線程池會新起一個線程來執(zhí)行這個新任務(wù),不管老線程是否空閑。

          你是如何理解核心線程的 ?

          從上一個問題可以看出,線程池雖說默認(rèn)是懶創(chuàng)建線程,但是它實際是想要快速擁有核心線程數(shù)的線程。核心線程指的是線程池承載日常任務(wù)的中堅力量,也就是說本質(zhì)上線程池是需要這么些數(shù)量的線程來處理任務(wù)的,所以在懶中又急著創(chuàng)建它。

          而最大線程數(shù)其實是為了應(yīng)付突發(fā)狀況。

          舉個裝修的例子,正常情況下施工隊只要 5 個人去干活,這 5 人其實就是核心線程,但是由于工頭接的活太多了,導(dǎo)致 5 個人在約定工期內(nèi)干不完,所以工頭又去找了 2 個人來一起干,所以 5 是核心線程數(shù),7 是最大線程數(shù)。

          平時就是 5 個人干活,特別忙的時候就找 7 個,等閑下來就會把多余的 2 個辭了。

          看到這里你可能會覺得核心線程在線程池里面會有特殊標(biāo)記?

          并沒有,不論是核心還是非核心線程,在線程池里面都是一視同仁,當(dāng)淘汰的時候不會管是哪些線程,反正留下核心線程數(shù)個線程即可,下文會作詳解。

          你是怎么理解 KeepAliveTime 的?

          這就是上面提到的,線程池其實想要的只是核心線程數(shù)個線程,但是又預(yù)留了一些數(shù)量來預(yù)防突發(fā)狀況,當(dāng)突發(fā)狀況過去之后,線程池希望只維持核心線程數(shù)的線程,所以就弄了個 KeepAliveTime,當(dāng)線程數(shù)大于核心數(shù)之后,如果線程空閑了一段時間(KeepAliveTime),就回收線程,直到數(shù)量與核心數(shù)持平。

          那 workQueue 有什么用?

          緩存任務(wù)供線程獲取,這里要注意限制工作隊列的大小。隊列長了,堆積的任務(wù)就多,堆積的任務(wù)多,后面任務(wù)等待的時長就長。

          想想你點擊一個按鈕是一直轉(zhuǎn)圈等半天沒反應(yīng)舒服,還是直接報錯舒服,所以有時心是好的,想盡量完成提交的任務(wù),但是用戶體驗不如直接拒絕。更有可能由于允許囤積的任務(wù)過多,導(dǎo)致資源耗盡而系統(tǒng)崩潰。

          所以工作隊列起到一個緩沖作用,具體隊列長度需要結(jié)合線程數(shù),任務(wù)的執(zhí)行時長,能承受的等待時間等。

          你是如何理解拒絕策略的?

          線程數(shù)總有拉滿的一天,工作隊列也是一樣,如果兩者都滿了,此時的提交任務(wù)就需要拒絕,默認(rèn)實現(xiàn)是 AbortPolicy 直接拋出異常。

          剩下的拒絕策略有直接丟棄任務(wù)一聲不吭的、讓提交任務(wù)的線程自己運行的、淘汰老的未執(zhí)行的任務(wù)而空出位置的,具體用哪個策略,根據(jù)場景選擇。當(dāng)然也可以自定義拒絕策略,實現(xiàn) RejectedExecutionHandler 這個接口即可。

          所以線程池盡可能只維護核心數(shù)量的線程,提供任務(wù)隊列暫存任務(wù),并提供拒絕策略來應(yīng)對過載的任務(wù)。

          這里還有個細節(jié),如果線程數(shù)已經(jīng)達到核心線程數(shù),那么新增加的任務(wù)只會往任務(wù)隊列里面塞,不會直接給予某個線程,如果任務(wù)隊列也滿了,新增最大線程數(shù)的線程時,任務(wù)是可以直接給予新建的線程執(zhí)行的,而不是入隊。

          感覺已經(jīng)會了?那再來看幾道面試題:

          你說你看過源碼,那你肯定知道線程池里的 ctl 是干嘛的咯?

          其實看下注釋就很清楚了,ctl 是一個涵蓋了兩個概念的原子整數(shù)類,它將工作線程數(shù)和線程池狀態(tài)結(jié)合在一起維護,低 29 位存放 workerCount,高 3 位存放 runState。

          其實并發(fā)包中有很多實現(xiàn)都是一個字段存多個值的,比如讀寫鎖的高 16 位存放讀鎖,低 16 位存放寫鎖,這種一個字段存放多個值可以更容易的維護多個值之間的一致性,也算是極簡主義。

          你知道線程池有幾種狀態(tài)嗎?

          注解說的很明白,我再翻譯一下:

          • RUNNING:能接受新任務(wù),并處理阻塞隊列中的任務(wù)
          • SHUTDOWN:不接受新任務(wù),但是可以處理阻塞隊列中的任務(wù)
          • STOP:不接受新任務(wù),并且不處理阻塞隊列中的任務(wù),并且還打斷正在運行任務(wù)的線程,就是直接撂擔(dān)子不干了!
          • TIDYING:所有任務(wù)都終止,并且工作線程也為0,處于關(guān)閉之前的狀態(tài)
          • TERMINATED:已關(guān)閉。

          你知道線程池的狀態(tài)是如何變遷的嗎?

          注釋里面也寫的很清楚,我再畫個圖

          為什么要把任務(wù)先放在任務(wù)隊列里面,而不是把線程先拉滿到最大線程數(shù)?

          我說下我的個人理解。

          其實經(jīng)過上面的分析可以得知,線程池本意只是讓核心數(shù)量的線程工作著,不論是 core 的取名,還是 keepalive 的設(shè)定,所以你可以直接把 core 的數(shù)量設(shè)為你想要線程池工作的線程數(shù),而任務(wù)隊列起到一個緩沖的作用。最大線程數(shù)這個參數(shù)更像是無奈之舉,在最壞的情況下做最后的努力,去新建線程去幫助消化任務(wù)。

          所以我個人覺得沒有為什么,就是這樣設(shè)計的,并且這樣的設(shè)定挺合理。

          當(dāng)然如果你想要扯一扯 CPU 密集和 I/O 密集,那可以扯一扯。

          原生版線程池的實現(xiàn)可以認(rèn)為是偏向 CPU 密集的,也就是當(dāng)任務(wù)過多的時候不是先去創(chuàng)建更多的線程,而是先緩存任務(wù),讓核心線程去消化,從上面的分析我們可以知道,當(dāng)處理 CPU 密集型任務(wù)的時,線程太多反而會由于線程頻繁切換的開銷而得不償失,所以優(yōu)先堆積任務(wù)而不是創(chuàng)建新的線程。

          而像 Tomcat 這種業(yè)務(wù)場景,大部分情況下是需要大量 I/O 處理的情況就做了一些定制,修改了原生線程池的實現(xiàn),使得在隊列沒滿的時候,可以創(chuàng)建線程至最大線程數(shù)。

          如何修改原生線程池,使得可以先拉滿線程數(shù)再入任務(wù)隊列排隊?

          如果了解線程池的原理,很輕松的就知道關(guān)鍵點在哪,就是隊列的 offer 方法。

          execute 方法想必大家都不陌生,就是給線程池提交任務(wù)的方法。在這個方法中可以看到只要在 offer 方法內(nèi)部判斷此時線程數(shù)還小于最大線程數(shù)的時候返回 false,即可走下面 else ifaddWorker (新增線程)的邏輯,如果數(shù)量已經(jīng)達到最大線程數(shù),直接入隊即可。

          詳細的我們可以看看 Tomcat 中是如何定制線程的。

          Tomcat 中的定制化線程池實現(xiàn)

          public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {}

          可以看到先繼承了 JUC 的線程池,然后我們重點關(guān)注一下 execute 這個方法

          這里可以看到,Tomcat 維護了一個 submittedCount 變量,這個變量的含義是統(tǒng)計已經(jīng)提交的但是還未完成的任務(wù)數(shù)量(記住這個變量,很關(guān)鍵),所以只要提交一個任務(wù),這個數(shù)就加一,并且捕獲了拒絕異常,再次嘗試將任務(wù)入隊,這個操作其實是為了盡可能的挽救回一些任務(wù),因為這么點時間差可能已經(jīng)執(zhí)行完很多任務(wù),隊列騰出了空位,這樣就不需要丟棄任務(wù)。

          然后我們再來看下代碼里出現(xiàn)的 TaskQueue,這個就是上面提到的定制關(guān)鍵點了。

          public class TaskQueue extends LinkedBlockingQueue<Runnable{
              private transient volatile ThreadPoolExecutor parent = null;
              ........
          }

          可以看到這個任務(wù)隊列繼承了 LinkedBlockingQueue,并且有個 ThreadPoolExecutor 類型的成員變量 parent ,我們再來看下 offer 方法的實現(xiàn),這里就是修改原來線程池任務(wù)提交與線程創(chuàng)建邏輯的核心了。

          從上面的邏輯可以看出是有機會在隊列還未滿的時候,先創(chuàng)建線程至最大線程數(shù)的!

          再補充一下,如果對直接返回 false 就能創(chuàng)建線程感到疑惑的話,往上翻一翻,上面貼了原生線程池 execute 的邏輯。

          然后上面的代碼其實只看到 submittedCount 的增加,正常的減少在 afterExecute 里實現(xiàn)了。

          而這個 afterExecute 在任務(wù)執(zhí)行完畢之后就會調(diào)用,與之對應(yīng)的還有個 beforeExecute,在任務(wù)執(zhí)行之前調(diào)用。

          至此,想必 Tomcat 中的定制化線程池的邏輯已經(jīng)明白了。

          如果線程池中的線程在執(zhí)行任務(wù)的時候,拋異常了,會怎么樣?

          嘿嘿,細心的同學(xué)想必已經(jīng)瞄到了上面的代碼,task.run() 被 try catch finally包裹,異常被扔到了 afterExecute 中,并且也繼續(xù)被拋了出來。

          而這一層外面,還有個try finally,所以異常的拋出打破了 while 循環(huán),最終會執(zhí)行 `processWorkerExit方法

          我們來看下這個方法,其實邏輯很簡單,把這個線程廢了,然后新建一個線程替換之。

          移除了引用等于銷毀了,這事兒 GC 會做的。

          所以如果一個任務(wù)執(zhí)行一半就拋出異常,并且你沒有自行處理這個異常,那么這個任務(wù)就這樣戛然而止了,后面也不會有線程繼續(xù)執(zhí)行剩下的邏輯,所以要自行捕獲和處理業(yè)務(wù)異常。

          addWorker 的邏輯就不分析了,就是新建一個線程,然后塞到 workers 里面,然后調(diào)用 start() 讓它跑起來。

          原生線程池的核心線程一定伴隨著任務(wù)慢慢創(chuàng)建的嗎?

          并不是,線程池提供了兩個方法:

          • prestartCoreThread:啟動一個核心線程
          • prestartAllCoreThreads :啟動所有核心線程

          不要小看這個預(yù)創(chuàng)建方法,預(yù)熱很重要,不然剛重啟的一些服務(wù)有時是頂不住瞬時請求的,就立馬崩了,所以有預(yù)熱線程、緩存等等操作。

          線程池的核心線程在空閑的時候一定不會被回收嗎?

          有個 allowCoreThreadTimeOut 方法,把它設(shè)置為 true ,則所有線程都會超時,不會有核心數(shù)那條線的存在。

          具體是會調(diào)用 interruptIdleWorkers 這個方法。

          這里需要講一下的是 w.tryLock() 這個方法,有些人可能會奇怪,Worker 怎么還能 lock。

          Worker 是屬于工作線程的封裝類,它不僅實現(xiàn)了 Runnable 接口,還繼承了 AQS。

          之所以要繼承 AQS 就是為了用上 lock 的狀態(tài),執(zhí)行任務(wù)的時候上鎖,任務(wù)執(zhí)行完了之后解鎖,這樣執(zhí)行關(guān)閉線程池等操作的時候可以通過 tryLock 來判斷此時線程是否在干活,如果 tryLock 成功說明此時線程是空閑的,可以安全的回收。

          interruptIdleWorkers 對應(yīng)的還有一個  interruptWorkers 方法,從名字就能看出差別,不空閑的 worker 也直接給打斷了。

          根據(jù)這兩個方法,又可以扯到 shutdown 和 shutdownNow,就是關(guān)閉線程池的方法,一個是安全的關(guān)閉線程池,會等待任務(wù)都執(zhí)行完畢,一個是粗暴的直接咔嚓了所有線程,管你在不在運行,兩個方法分別調(diào)用的就是 interruptIdleWorkers() 和 interruptWorkers() 來中斷線程。

          這又可以引申出一個問題,shutdownNow 了之后還在任務(wù)隊列中的任務(wù)咋辦?眼尖的小伙伴應(yīng)該已經(jīng)看到了,線程池還算負(fù)責(zé),把未執(zhí)行的任務(wù)拖拽到了一個列表中然后返回,至于怎么處理,就交給調(diào)用者了!

          詳細就是上面的 drainQueue 方法。

          這里可能又會有同學(xué)有疑問,都 drainTo 了,為什么還要判斷一下隊列是否為空,然后進行循環(huán)?

          那是因為如果隊列是 DelayQueue 或任何其他類型的隊列,其中 poll 或 drainTo 可能無法刪除某些元素,所以需要遍歷,逐個刪除它們。

          回到最開始的面試題

          線程池如何動態(tài)修改核心線程數(shù)和最大線程數(shù)?

          其實之所以會有這樣的需求是因為線程數(shù)是真的不好配置。

          你可能會在網(wǎng)上或者書上看到很多配置公式,比如:

          • CPU 密集型的話,核心線程數(shù)設(shè)置為 CPU核數(shù)+1
          • I/O 密集型的話,核心線程數(shù)設(shè)置為 2*CPU核數(shù)

          比如:

          線程數(shù)=CPU核數(shù) *(1+線程等待時間 / 線程時間運行時間)

          這個比上面的更貼合與業(yè)務(wù),還有一些理想的公式就不列了。就這個公式而言,這個線程等待時間就很難測,拿 Tomcat 線程池為例,每個請求的等待時間能知道?不同的請求不同的業(yè)務(wù),就算相同的業(yè)務(wù),不同的用戶數(shù)據(jù)量也不同,等待時間也不同。

          所以說線程數(shù)真的很難通過一個公式一勞永逸,線程數(shù)的設(shè)定是一個迭代的過程,需要壓測適時調(diào)整,以上的公式做個初始值開始調(diào)試是 ok 的。

          再者,流量的突發(fā)性也是無法判斷的,舉個例子 1 秒內(nèi)一共有 1000 個請求量,但是如果這 1000 個請求量都是在第一毫秒內(nèi)瞬時進來的呢?

          這就很需要線程池的動態(tài)性,也是這個上面這個面試題的需求來源。

          原生的線程池核心我們大致都過了一遍,不過這幾個方法一直沒提到,先來看看這幾個方法:

          我就不一一翻譯了,大致可以看出線程池其實已經(jīng)給予方法暴露出內(nèi)部的一些狀態(tài),例如正在執(zhí)行的線程數(shù)、已完成的任務(wù)數(shù)、隊列中的任務(wù)數(shù)等等。

          當(dāng)然你可以想要更多的數(shù)據(jù)監(jiān)控都簡單的,像 Tomcat 那種繼承線程池之后自己加唄,動態(tài)調(diào)整的第一步監(jiān)控就這樣搞定了!定時拉取這些數(shù)據(jù),然后搞個看板,再結(jié)合郵件、短信、釘釘?shù)葓缶绞?,我們可以很容易的監(jiān)控線程池的狀態(tài)!

          接著就是動態(tài)修改線程池配置了。

          可以看到線程池已經(jīng)提供了諸多修改方法來更改線程池的配置,所以李老都已經(jīng)考慮到啦!

          同樣,也可以繼承線程池增加一些方法來修改,看具體的業(yè)務(wù)場景了。同樣搞個頁面,然后給予負(fù)責(zé)人員配置修改即可。

          所以原生線程池已經(jīng)提供修改配置的方法,也對外暴露出線程池內(nèi)部執(zhí)行情況,所以只要我們實時監(jiān)控情況,調(diào)用對應(yīng)的 set 方法,即可動態(tài)修改線程池對應(yīng)配置。

          回答面試題的時候一定要提監(jiān)控,顯得你是有的放矢的。

          如果你是 JDK 設(shè)計者,如何設(shè)計?

          其實我覺得這個是緊接著上一題問的,應(yīng)該算是同一個問題。

          而且 JDK 設(shè)計者已經(jīng)設(shè)計好了呀?所以怎么說我也不清楚,不過我們可以說一說具體的實現(xiàn)邏輯唄。

          先來看下設(shè)置核心線程數(shù)的方法:

          隨著注釋看下來應(yīng)該沒什么問題,就是那個 k 值我說一下,李老覺得核心線程數(shù)是配置了,但是此時線程池內(nèi)部是否需要這么多線程是不確定的,那么就按工作隊列里面的任務(wù)數(shù)來,直接按任務(wù)數(shù)立刻新增線程,當(dāng)任務(wù)隊列為空了之后就終止新增。

          這其實和李老設(shè)計的默認(rèn)核心線程數(shù)增加策略是一致的,都是懶創(chuàng)建線程。

          再看看設(shè)置最大線程數(shù)的方法:

          沒啥花頭,調(diào)用的 interruptIdleWorkers 之前都分析過了。

          我再貼一下之前寫過的線程池設(shè)計面試題吧。

          如果要讓你設(shè)計一個線程池,你要怎么設(shè)計?

          這種設(shè)計類問題還是一樣,先說下理解,表明你是知道這個東西的用處和原理的,然后開始 BB。基本上就是按照現(xiàn)有的設(shè)計來說,再添加一些個人見解。

          線程池講白了就是存儲線程的一個容器,池內(nèi)保存之前建立過的線程來重復(fù)執(zhí)行任務(wù),減少創(chuàng)建和銷毀線程的開銷,提高任務(wù)的響應(yīng)速度,并便于線程的管理。

          我個人覺得如果要設(shè)計一個線程池的話得考慮池內(nèi)工作線程的管理、任務(wù)編排執(zhí)行、線程池超負(fù)荷處理方案、監(jiān)控。

          初始化線程數(shù)、核心線程數(shù)、最大線程池都暴露出來可配置,包括超過核心線程數(shù)的線程空閑消亡配置。

          任務(wù)的存儲結(jié)構(gòu)可配置,可以是無界隊列也可以是有界隊列,也可以根據(jù)配置分多個隊列來分配不同優(yōu)先級的任務(wù),也可以采用 stealing 的機制來提高線程的利用率。

          再提供配置來表明此線程池是 IO 密集還是 CPU 密集型來改變?nèi)蝿?wù)的執(zhí)行策略。

          超負(fù)荷的方案可以有多種,包括丟棄任務(wù)、拒絕任務(wù)并拋出異常、丟棄最舊的任務(wù)或自定義等等。

          線程池埋好點暴露出用于監(jiān)控的接口,如已處理任務(wù)數(shù)、待處理任務(wù)數(shù)、正在運行的線程數(shù)、拒絕的任務(wù)數(shù)等等信息。

          我覺得基本上這樣答就差不多了,等著面試官的追問就好。

          注意不需要跟面試官解釋什么叫核心線程數(shù)之類的,都懂的沒必要。

          當(dāng)然這種開放型問題還是仁者見仁智者見智,我這個不是標(biāo)準(zhǔn)答案,僅供參考。

          關(guān)于線程池的一點碎碎念

          線程池的好處我們都知道了,但是不是任何時刻都上線程池的,我看過一些奇怪的代碼,就是為了用線程池而用線程池...

          還有需要根據(jù)不同的業(yè)務(wù)劃分不同的線程池,不然會存在一些耗時的業(yè)務(wù)影響了另一個業(yè)務(wù)導(dǎo)致這個業(yè)務(wù)崩了,然后都崩了的情況,所以要做好線程池隔離。

          最后

          好了,有關(guān)線程池的知識點和一些常見的一些面試題應(yīng)該都涉及到了吧,如果還有別的啥角度刁鉆的面試題,歡迎留言提出,咱們一起研究研究。

          相信看了這篇文章之后,關(guān)于線程池出去面試可以開始吹了!

          如果覺得文章不錯,來個點贊和在看唄!

          — 【 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ā)吧。

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

          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  丰满级A片直播免费下载观看 | 一级片黄色片 | 亚州第一成人网站 | 女人三级视屏 | 亚洲激情乱伦小说 |