Java之線程池ThreadPoolExecutor技術(shù)

  • 近年來由于互聯(lián)網(wǎng)的興起,所以現(xiàn)在Java 面試題中也經(jīng)常會(huì)問到線程池技術(shù),所以今天我們就說一說面試中經(jīng)常問道的知識(shí)點(diǎn)。

一 、 基礎(chǔ)知識(shí)

  1. 為什么要使用線程池呢?
    • 在實(shí)際使用中,線程是很占用系統(tǒng)資源的,如果對(duì)線程管理不善很容易導(dǎo)致系統(tǒng)問題。因此,在大多數(shù)并發(fā)框架中都會(huì)使用線程池技術(shù)來管理線程,那么使用線程池管理線程主要有下面三點(diǎn)好處:
      1. 降低資源消耗。 通過復(fù)用已經(jīng)存在的線程和降低線程關(guān)閉的次數(shù)來盡可能降低系統(tǒng)的消耗;
      2. 提升系統(tǒng)響應(yīng)速度。通過復(fù)用線程,省去創(chuàng)建線程的過程,因此整體上提升了系統(tǒng)的響應(yīng)速度。
      3. 提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)的資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,因此,需要使用線程池管理線程。
  2. Java 線程的整體架構(gòu)體系
    1. Java中的線程池是通過Executor框架實(shí)現(xiàn)的,該框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor這幾個(gè)類 。


      image.png
    2. 三個(gè)JDK 線程實(shí)現(xiàn)類
      • Executors.newFixedThreadPool(int):執(zhí)行長期任務(wù)性能好,創(chuàng)建一個(gè)線程池,一個(gè)池中有N個(gè)固定線程,有固定線程數(shù)的線程池。
        // 代碼實(shí)現(xiàn)  模擬十個(gè)人去銀行辦理業(yè)務(wù),然后有5個(gè)窗口進(jìn)行業(yè)務(wù)
        ExecutorService executorService = Executors.newFixedThreadPool(5); //固定線程數(shù)
        for (int i = 0; i < 10; i++) {
//            Thread.sleep(20);
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+" 辦理業(yè)務(wù)");
            });
        }
        executorService.shutdown();
image.png
  • Executors.newSingleThreadExecutor():一個(gè)一個(gè)任務(wù)的執(zhí)行,一池一線程
        // 代碼實(shí)現(xiàn) 
        ExecutorService executorService = Executors.newSingleThreadExecutor(); //一池子一個(gè)工作線程類似銀行只有一個(gè)窗口
        for (int i = 0; i < 10; i++) {
//            Thread.sleep(20);
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+" 辦理業(yè)務(wù)");
            });
        }
        executorService.shutdown();
image.png
  • Executors.newCachedThreadPool(): 執(zhí)行很
    多短期任務(wù),線程池根據(jù)需要?jiǎng)?chuàng)建新線程,但是先前構(gòu)建的線程可用時(shí)將他重用起來,可擴(kuò)容。
      // 代碼實(shí)現(xiàn)
       ExecutorService executorService = Executors.newCachedThreadPool(); //可擴(kuò)容線程池
        for (int i = 0; i < 10; i++) {
//            Thread.sleep(20);
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+" 辦理業(yè)務(wù)");
            });
        }
        executorService.shutdown();
image.png
  • 以上三個(gè)實(shí)現(xiàn)方法是Java 給我們帶的,但是如果在面試的時(shí)候面試官問你線程池的技術(shù)你要是管說這三個(gè)實(shí)現(xiàn),基本上你就掛了。其實(shí)我們看他們?nèi)齻€(gè)的底層實(shí)現(xiàn)都是一個(gè)類那就是ThreadPoolExecutor


    image.png

    image.png

    image.png
  1. ThreadPoolExecutor
  • 那么我們看看那個(gè)ThreadPoolExecutor 類里面的內(nèi)容,下面就是線程池的源碼,那我們現(xiàn)在就看看這些源碼。


    image.png
  • 我們先看一下線程池的主要參數(shù),7個(gè)參數(shù)
    1. corePoolSize :線程池中常駐核心線程數(shù)
    2. maximumPoolSize:線程池中能夠容納同時(shí)執(zhí)行最大的線程數(shù),此值必須大于等于1
    3. keepAliveTime: 多余的空閑線程的存貨時(shí)間,達(dá)到keepAliveTime時(shí),多余線程會(huì)被銷毀直到剩下corePoolSize個(gè)線程數(shù)為止。
    4. unit:keepAliveTime 的單位
    5. workQueue:任務(wù)隊(duì)列,被提交但未被執(zhí)行的任務(wù)。
    6. threadFactory:表示生成線程池中工作線程的線程工廠,用于創(chuàng)建線程,一般默認(rèn)即可。
    7. handler:拒絕策略,表示當(dāng)隊(duì)列滿了,并且工作線程大于等于線程池中最大的線程數(shù)(maximumPoolSize)時(shí)如何拒絕執(zhí)行的runable的策略。
  • 以上7個(gè)參數(shù)我們都是必須要掌握的,而且要知道為什么這么設(shè)置,所以接下來我們我們要看一下線程池的底層原理。


    image.png
  • 線程池的底層工作原理(以下很重要,包括上面的圖也是很重要的)
    1. 在創(chuàng)建線程池后,開始等待請(qǐng)求。
    2. 在調(diào)用execute()方法添加一個(gè)請(qǐng)求任務(wù)時(shí),線程池會(huì)做出如下判斷:
      2.1 如果正在運(yùn)行的線程數(shù)量小于corePoolSize,那么馬上會(huì)創(chuàng)建線程運(yùn)行任務(wù);
      2.2 如果正在運(yùn)行的線程數(shù)量大于或等于corePoolSize,那么將這個(gè)任務(wù)放入隊(duì)列;
      2.3 如果這個(gè)時(shí)候隊(duì)列滿了且正在運(yùn)行的線程數(shù)量還小于maximumPoolSize,那么還是要?jiǎng)?chuàng)建非線程立刻運(yùn)行這個(gè)任務(wù);
      2.4 如果隊(duì)列滿了且正在運(yùn)行的線程數(shù)量大于或者等于maximumPoolSize,那么線程池會(huì)啟動(dòng)飽和拒絕策略來執(zhí)行。
    3. 當(dāng)一個(gè)線程完成任務(wù)時(shí),它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來執(zhí)行。
    4. 當(dāng)一個(gè)線程無事可做超過一定的時(shí)間(keepAliveTime)時(shí),線程會(huì)判斷:如果當(dāng)前運(yùn)行的線程數(shù)量大于corePoolSize,那么這個(gè)線程就會(huì)被停掉。所以線程池的所有任務(wù)完成后,它最終會(huì)收縮到corePoolSize的大小。
  • 以上時(shí)線程池執(zhí)行的整體過程,下面要看一下線程的處理過程。


    image.png
  1. 線程池用哪個(gè)?生產(chǎn)中如設(shè)置合理參數(shù)呢 ?
    1. 線程池的拒絕策略 。
      • 什么是線程池的拒絕策略呢:等待隊(duì)列已經(jīng)排滿了,再也塞不下新任務(wù)了同時(shí),線程池中的max線程也達(dá)到了,無法繼續(xù)為新任務(wù)服務(wù)。這個(gè)是時(shí)候我們就需要拒絕策略機(jī)制合理的處理這個(gè)問題。
      • JDK內(nèi)置的4個(gè)拒絕策略(以下內(nèi)置拒絕策略均實(shí)現(xiàn)了
        RejectedExecutionHandle接口)
        1. AbortPolicy(默認(rèn)):直接拋出RejectedExecutionException異常阻止系統(tǒng)正常運(yùn)行
        2. CallerRunsPolicy:“調(diào)用者運(yùn)行”一種調(diào)節(jié)機(jī)制,該策略既不會(huì)拋棄任務(wù),也不
          會(huì)拋出異常,而是將某些任務(wù)回退到調(diào)用者,從而降低新任務(wù)的流量。
        3. DiscardOldestPolicy:拋棄隊(duì)列中等待最久的任務(wù),然后把當(dāng)前任務(wù)加人隊(duì)列中嘗試再次提交當(dāng)前任務(wù)。
        4. DiscardPolicy:該策略默默地丟棄無法處理的任務(wù),不予任何處理也不拋出異常。如果允許任務(wù)丟失,這是最好的一種策略。
    2. 工作中我們選擇什么線程池呢? 這個(gè)是面試常問的考點(diǎn),如果你要是回答JDK默認(rèn)的三種線程池技術(shù)你就完蛋了,代表你只是了解一些,根本沒有用過。
      • 答案是一個(gè)都不用,工作中只能用自定義的。
      • JDK 都提供了為什么不用呢?
        • 這就跟源碼有關(guān)系了,我們看看源碼。
      //Executors.newFixedThreadPool() 源碼
      public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
      }
      
      // Executors.newSingleThreadExecutor()
      public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()));
      }
      
      //Executors.newCachedThreadPool()
      public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
      }
      
       - 看看JDK給我們自帶的實(shí)現(xiàn),之前我們說過7大參數(shù),那么現(xiàn)在看看5個(gè)參數(shù) 其中這些參數(shù)設(shè)計(jì)的及其不合理(newCachedThreadPool 中的maximumPoolSize設(shè)置的是Integer.MAX_VALUE,有些太大了,這樣會(huì)導(dǎo)致系統(tǒng)失敗的),根本不符合企業(yè)的要求,所以我們要自定義。
      
      • 自定義線程池(代碼要會(huì)手寫)
      ThreadPoolExecutor threadPoolExecutor = new         ThreadPoolExecutor(
              2,
              5,
              3L,
              TimeUnit.SECONDS,
              new LinkedBlockingQueue<>(3),
              Executors.defaultThreadFactory(),
              new ThreadPoolExecutor.DiscardPolicy());
      
      for (int i = 0; i < 9; i++) {
      //Thread.sleep(20);
          threadPoolExecutor.execute(()->{
              System.out.println(Thread.currentThread().getName()+" 辦理業(yè)務(wù)");
          });
      }
      threadPoolExecutor.shutdown();
      
      • 但是這些參數(shù)我們要怎么設(shè)置呢? 其中最重要的就是配置最大線程數(shù)
        1. 如果線程池要處理的任務(wù)是cpu密集型,那么最大的任務(wù)就是cpu核數(shù)+ 1(但是我們不能寫死了 要使用代碼自動(dòng)獲取Runtime.getRuntime().availableProcessors())。
        2. 如果線程池要處理的任務(wù)是IO密集型,那么最大的任務(wù)就是cpu核數(shù)/阻塞系數(shù)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容