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

          深入源碼,深度解析Java 線程池的實(shí)現(xiàn)原理

          共 11023字,需瀏覽 23分鐘

           ·

          2021-05-29 07:57

          △Hollis, 一個(gè)對Coding有著獨(dú)特追求的人△

          這是Hollis的第 356 篇原創(chuàng)分享

          作者 l zyz1992

          來源 l Hollis(ID:hollischuang)

          Java 系統(tǒng)的運(yùn)行歸根到底是程序的運(yùn)行,程序的運(yùn)行歸根到底是代碼的執(zhí)行,代碼的執(zhí)行歸根到底是虛擬機(jī)的執(zhí)行,虛擬機(jī)的執(zhí)行其實(shí)就是操作系統(tǒng)的線程在執(zhí)行,并且會占用一定的系統(tǒng)資源,如CPU、內(nèi)存、磁盤、網(wǎng)絡(luò)等等。所以,如何高效的使用這些資源就是程序員在平時(shí)寫代碼時(shí)候的一個(gè)努力的方向。本文要說的線程池就是一種對 CPU 利用的優(yōu)化手段。
          線程池,百度百科是這么解釋的:
          線程池是一種多線程處理形式,處理過程中將任務(wù)添加到隊(duì)列,然后在創(chuàng)建線程后自動啟動這些任務(wù)。線程池線程都是后臺線程。每個(gè)線程都使用默認(rèn)的堆棧大小,以默認(rèn)的優(yōu)先級運(yùn)行,并處于多線程單元中。如果某個(gè)線程在托管代碼中空閑(如正在等待某個(gè)事件),則線程池將插入另一個(gè)輔助線程來使所有處理器保持繁忙。如果所有線程池線程都始終保持繁忙,但隊(duì)列中包含掛起的工作,則線程池將在一段時(shí)間后創(chuàng)建另一個(gè)輔助線程但線程的數(shù)目永遠(yuǎn)不會超過最大值。超過最大值的線程可以排隊(duì),但他們要等到其他線程完成后才啟動。
          線程池,其實(shí)就是維護(hù)了很多線程的池子,類似這樣的池化技術(shù)還有很多的,例如:HttpClient 連接池、數(shù)據(jù)庫連接池、內(nèi)存池等等。


          線程池的優(yōu)點(diǎn)

          在 Java 并發(fā)編程框架中的線程池是運(yùn)用場景最多的技術(shù),幾乎所有需要異步或并發(fā)執(zhí)行任務(wù)的程序都可以使用線程池。在開發(fā)過程中,合理地使用線程池能夠帶來至少以下4個(gè)好處。

          第一:降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗;

          第二:提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行;

          第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一分配、調(diào)優(yōu)和監(jiān)控。

          第四:提供更強(qiáng)大的功能。比如延時(shí)定時(shí)線程池;


          線程池的實(shí)現(xiàn)原理

          當(dāng)向線程池提交一個(gè)任務(wù)之后,線程池是如何處理這個(gè)任務(wù)的呢?下面就先來看一下它的主要處理流程。先來看下面的這張圖,然后我們一步一步的來解釋。

          當(dāng)使用者將一個(gè)任務(wù)提交到線程池以后,線程池是這么執(zhí)行的:

          ①首先判斷核心的線程數(shù)是否已滿,如果沒有滿,那么就去創(chuàng)建一個(gè)線程去執(zhí)行該任務(wù);否則請看下一步

          ②如果線程池的核心線程數(shù)已滿,那么就繼續(xù)判斷任務(wù)隊(duì)列是否已滿,如果沒滿,那么就將任務(wù)放到任務(wù)隊(duì)列中;否則請看下一步

          ③如果任務(wù)隊(duì)列已滿,那么就判斷線程池是否已滿,如果沒滿,那么就創(chuàng)建線程去執(zhí)行該任務(wù);否則請看下一步;

          ④如果線程池已滿,那么就根據(jù)拒絕策略來做出相應(yīng)的處理;

          上面的四步其實(shí)就已經(jīng)將線程池的執(zhí)行原理描述結(jié)束了。如果不明白沒有關(guān)系,先一步一步往下看,上面涉及到的線程池的專有名詞都會詳細(xì)的介紹到。

          我們在平時(shí)的開發(fā)中,線程池的使用基本都是基于ThreadPoolExexutor 類,他的繼承體系是這樣子的:

          image-20210322133058425

          那既然說在使用中都是基于 ThreadPoolExexutor 的那么我們就重點(diǎn)分析這個(gè)類。

          至于他構(gòu)造體系中的其他的類或者是接口中的屬性,這里就不去截圖了,完全沒有必要。小伙伴如果實(shí)在想看就自己去打開代碼看一下就行了。


          ThreadPoolExecutor

          在《阿里巴巴 java 開發(fā)手冊》中指出了線程資源必須通過線程池提供,不允許在應(yīng)用中自行顯示的創(chuàng)建線程,這樣一方面是線程的創(chuàng)建更加規(guī)范,可以合理控制開辟線程的數(shù)量;另一方面線程的細(xì)節(jié)管理交給線程池處理,優(yōu)化了資源的開銷。

          其原文描述如下:

          在ThreadPoolExecutor類中提供了四個(gè)構(gòu)造方法,但是他的四個(gè)構(gòu)造器中,實(shí)際上最終都會調(diào)用同一個(gè)構(gòu)造器,只不過是在另外三個(gè)構(gòu)造器中,如果有些參數(shù)不傳ThreadPoolExecutor會幫你使用默認(rèn)的參數(shù)。所以,我們直接來看這個(gè)完整參數(shù)的構(gòu)造器,來徹底剖析里面的參數(shù)。

          public  class  ThreadPoolExecutor  extends  AbstractExecutorService {
              ......

              public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
                                            RejectedExecutionHandler handler)
           
          {
                      if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0){
                          throw new IllegalArgumentException();
                      }
                      if (workQueue == null || threadFactory == null || handler == null){
                          throw new NullPointerException();
                      }
                      this.acc = System.getSecurityManager() == null ? null : AccessController.getContext();
                      this.corePoolSize = corePoolSize;
                      this.maximumPoolSize = maximumPoolSize;
                      this.workQueue = workQueue;
                      this.keepAliveTime = unit.toNanos(keepAliveTime);
                      this.threadFactory = threadFactory;
                      this.handler = handler;
                  }
          }

          主要參數(shù)就是下面這幾個(gè):

          • corePoolSize:線程池中的核心線程數(shù),包括空閑線程,也就是核心線程數(shù)的大小;

          • maximumPoolSize:線程池中允許的最多的線程數(shù),也就是說線程池中的線程數(shù)是不可能超過該值的;

          • keepAliveTime:當(dāng)線程池中的線程數(shù)大于 corePoolSize 的時(shí)候,在超過指定的時(shí)間之后就會將多出 corePoolSize 的的空閑的線程從線程池中刪除;

          • unit:keepAliveTime 參數(shù)的單位(常用的秒為單位);

          • workQueue:用于保存任務(wù)的隊(duì)列,此隊(duì)列僅保持由 executor 方法提交的任務(wù) Runnable 任務(wù);

          • threadFactory線程池工廠,他主要是為了給線程起一個(gè)標(biāo)識。也就是為線程起一個(gè)具有意義的名稱;

          • handler拒絕策略

          阻塞隊(duì)列

          workQueue 有多種選擇,在 JDK 中一共提供了 7 中阻塞對列,分別為:

          • ArrayBlockingQueue一個(gè)由數(shù)組結(jié)構(gòu)組成的有界阻塞隊(duì)列。此隊(duì)列按照先進(jìn)先出(FIFO)的原則對元素進(jìn)行排序。默認(rèn)情況下不保證訪問者公平地訪問隊(duì)列 ,所謂公平訪問隊(duì)列是指阻塞的線程,可按照阻塞的先后順序訪問隊(duì)列。非公平性是對先等待的線程是不公平的,當(dāng)隊(duì)列可用時(shí),阻塞的線程都可以競爭訪問隊(duì)列的資格。

          • LinkedBlockingQueue一個(gè)由鏈表結(jié)構(gòu)組成的有界阻塞隊(duì)列。此隊(duì)列的默認(rèn)和最大長度為Integer.MAX_VALUE。此隊(duì)列按照先進(jìn)先出的原則對元素進(jìn)行排序。

          • PriorityBlockingQueue一個(gè)支持優(yōu)先級排序的無界阻塞隊(duì)列。(雖然此隊(duì)列邏輯上是無界的,但是資源被耗盡時(shí)試圖執(zhí)行 add 操作也將失敗,導(dǎo)致 OutOfMemoryError)

          • DelayQueue一個(gè)使用優(yōu)先級隊(duì)列實(shí)現(xiàn)的無界阻塞隊(duì)列。元素的一個(gè)無界阻塞隊(duì)列,只有在延遲期滿時(shí)才能從中提取元素

          • SynchronousQueue:一個(gè)不存儲元素的阻塞隊(duì)列。一種阻塞隊(duì)列,其中每個(gè)插入操作必須等待另一個(gè)線程的對應(yīng)移除操作 ,反之亦然。(SynchronousQueue 該隊(duì)列不保存元素)

          • LinkedTransferQueue一個(gè)由鏈表結(jié)構(gòu)組成的無界阻塞隊(duì)列。相對于其他阻塞隊(duì)列LinkedTransferQueue多了tryTransfer和transfer方法。

          • LinkedBlockingDeque一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列。是一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列

          在以上的7個(gè)隊(duì)列中,線程池中常用的是ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue隊(duì)列中的常用的方法如下:

          關(guān)于阻塞隊(duì)列,介紹到這里也就基本差不多了。

          線程池工廠

          線程池工廠,就像上面已經(jīng)介紹的,目的是為了給線程起一個(gè)有意義的名字。用起來也非常的簡單,只需要實(shí)現(xiàn)ThreadFactory接口即可

          public class CustomThreadFactory implements ThreadFactory {
              @Override
              public Thread newThread(Runnable r) {
                  Thread thread = new Thread(r);
                  thread.setName("我是你們自己定義的線程名稱");
                  return thread;
              }
          }

          具體的使用就不去廢話了。

          拒絕策略

          線程池有四種默認(rèn)的拒絕策略,分別為:

          • AbortPolicy:這是線程池默認(rèn)的拒絕策略,在任務(wù)不能再提交的時(shí)候,拋出異常,及時(shí)反饋程序運(yùn)行狀態(tài)。如果是比較關(guān)鍵的業(yè)務(wù),推薦使用此拒絕策略,這樣子在系統(tǒng)不能承載更大的并發(fā)量的時(shí)候,能夠及時(shí)的通過異常發(fā)現(xiàn);

          • DiscardPolicy:丟棄任務(wù),但是不拋出異常。如果線程隊(duì)列已滿,則后續(xù)提交的任務(wù)都會被丟棄,且是靜默丟棄。這玩意不建議使用;

          • DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新提交被拒絕的任務(wù)。這玩意不建議使用;

          • CallerRunsPolicy:如果任務(wù)添加失敗,那么主線程就會自己調(diào)用執(zhí)行器中的 executor 方法來執(zhí)行該任務(wù)。這玩意不建議使用;

          也就是說關(guān)于線程池的拒絕策略,最好使用默認(rèn)的。這樣能夠及時(shí)發(fā)現(xiàn)異常。如果上面的都不能滿足你的需求,你也可以自定義拒絕策略,只需要實(shí)現(xiàn) RejectedExecutionHandler 接口即可

          public class CustomRejection implements RejectedExecutionHandler {
              @Override
              public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                  System.out.println("你自己想怎么處理就怎么處理");
              }
          }

          看到這里,我們再來畫一張圖來總結(jié)和概括下線程池的執(zhí)行示意圖:

          詳細(xì)的執(zhí)行過程全部在圖中說明了。


          提交任務(wù)到線程池

          在 java 中,有兩個(gè)方法可以將任務(wù)提交到線程池,分別是submit和execute。

          execute 方法

          execute()方法用于提交不需要返回值的任務(wù),所以無法判斷任務(wù)是否被線程池執(zhí)行成功。

          void execute(Runnable command);

          通過以下代碼可知 execute() 方法輸入的任務(wù)是一個(gè)Runnable類的實(shí)例。

          executorService.execute(()->{
                      System.out.println("ThreadPoolDemo.execute");
                  });

          submit 方法

          submit()方法用于提交需要返回值的任務(wù)。

          Future<?> submit(Runnable task);

          線程池會返回一個(gè)future類型的對象,通過這個(gè) future 對象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值,get() 方法會阻塞當(dāng)前線程直到任務(wù)完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當(dāng)前線程一段時(shí)間后立即返回,這時(shí)候有可能任務(wù)沒有執(zhí)行完。

          Future<?> submit = executorService.submit(() -> {
                      System.out.println("ThreadPoolDemo.submit");
                  });



          關(guān)閉線程池

          其實(shí),如果優(yōu)雅的關(guān)閉線程池是一個(gè)令人頭疼的問題,線程開啟是簡單的,但是想要停止卻不是那么容易的。通常而言, 大部分程序員都是使用 jdk 提供的兩個(gè)方法來關(guān)閉線程池,他們分別是:shutdown 或 shutdownNow;

          通過調(diào)用線程池的 shutdown 或 shutdownNow 方法來關(guān)閉線程池。它們的原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的 interrupt 方法來中斷線程(PS:中斷,僅僅是給線程打上一個(gè)標(biāo)記,并不是代表這個(gè)線程停止了,如果線程不響應(yīng)中斷,那么這個(gè)標(biāo)記將毫無作用),所以無法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無法終止。

          但是它們存在一定的區(qū)別,shutdownNow首先將線程池的狀態(tài)設(shè)置成 STOP,然后嘗試停止所有的正在執(zhí)行或暫停任務(wù)的線程,并返回等待執(zhí)行任務(wù)的列表,而 shutdown 只是將線程池的狀態(tài)設(shè)置成SHUTDOWN狀態(tài),然后中斷所有沒有正在執(zhí)行任務(wù)的線程。

          只要調(diào)用了這兩個(gè)關(guān)閉方法中的任意一個(gè),isShutdown 方法就會返回 true。當(dāng)所有的任務(wù)都已關(guān)閉后,才表示線程池關(guān)閉成功,這時(shí)調(diào)用isTerminaed方法會返回 true。至于應(yīng)該調(diào)用哪一種方法來關(guān)閉線程池,應(yīng)該由提交到線程池的任務(wù)特性決定,通常調(diào)用 shutdown方法來關(guān)閉線程池,如果任務(wù)不一定要執(zhí)行完,則可以調(diào)用 shutdownNow 方法。

          這里推薦使用穩(wěn)妥的 shutdownNow 來關(guān)閉線程池,至于更優(yōu)雅的方式我會在以后的并發(fā)編程設(shè)計(jì)模式中的兩階段終止模式中會再次詳細(xì)介紹。


          合理的參數(shù)

          為什么叫合理的參數(shù),那不合理的參數(shù)是什么樣子的?在我們創(chuàng)建線程池的時(shí)候,里面的參數(shù)該如何設(shè)置才能稱之為合理呢?其實(shí)這是有一定的依據(jù)的,我們先來看一下以下的創(chuàng)建的方式:

          ExecutorService executorService = new ThreadPoolExecutor(5,
                          5,
                          5,
                          TimeUnit.SECONDS,
                          new ArrayBlockingQueue<>(5),
                          r -> {
                              Thread thread = new Thread(r);
                              thread.setName("線程池原理講解");
                              return thread;
                          });

          你說他合理不合理?我也不知道,因?yàn)槲覀儧]有參考的依據(jù),在實(shí)際的開發(fā)中,我們需要根據(jù)任務(wù)的性質(zhì)(IO是否頻繁?)來決定我們創(chuàng)建的核心的線程數(shù)的大小,實(shí)際上可以從以下的一個(gè)角度來分析:

          • 任務(wù)的性質(zhì):CPU密集型任務(wù)、IO密集型任務(wù)和混合型任務(wù);

          • 任務(wù)的優(yōu)先級:高、中和低;

          • 任務(wù)的執(zhí)行時(shí)間:長、中和短;

          • 任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接;

          性質(zhì)不同的任務(wù)可以用不同規(guī)模的線程池分開處理。分為CPU密集型和IO密集型。

          CPU密集型任務(wù)應(yīng)配置盡可能小的線程,如配置 Ncpu+1個(gè)線程的線程池。(可以通過Runtime.getRuntime().availableProcessors()來獲取CPU物理核數(shù))

          IO密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),則應(yīng)配置盡可能多的線程,如 2*Ncpu。

          混合型的任務(wù),如果可以拆分,將其拆分成一個(gè)CPU密集型任務(wù)一個(gè)IO密集型任務(wù),只要這兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不是太大,那么分解后執(zhí)行的吞吐量將高于串行執(zhí)行的吞吐量。

          如果這兩個(gè)任務(wù)執(zhí)行時(shí)間相差太大,則沒必要進(jìn)行分解。可以通過 Runtime.getRuntime().availableProcessors() 方法獲得當(dāng)前設(shè)備的CPU個(gè)數(shù)。

          優(yōu)先級不同的任務(wù)可以使用優(yōu)先級隊(duì)列 PriorityBlockingQueue來處理。它可以讓優(yōu)先級高的任務(wù)先執(zhí)行(注意:如果一直有優(yōu)先級高的任務(wù)提交到隊(duì)列里,那么優(yōu)先級低的任務(wù)可能永遠(yuǎn)不能執(zhí)行

          執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池來處理,或者可以使用優(yōu)先級隊(duì)列,讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行。依賴數(shù)據(jù)庫連接池的任務(wù),因?yàn)榫€程提交SQL后需要等待數(shù)據(jù)庫返回結(jié)果,等待的時(shí)間越長,則 CPU 空閑時(shí)間就越長,那么線程數(shù)應(yīng)該設(shè)置得越大,這樣才能更好地利用CPU。

          建議使用有界隊(duì)列。有界隊(duì)列能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力,可以根據(jù)需要設(shè)大一點(diǎn)。方式因?yàn)樘峤坏娜蝿?wù)過多而導(dǎo)致 OOM


          小結(jié)

          本文主要介紹的是線程池的實(shí)現(xiàn)原理以及一些使用技巧,在實(shí)際開發(fā)中,線程池可以說是稍微高級一點(diǎn)的程序員的必備技能。所以掌握好線程池這門技術(shù)也是重中之重!

          另外,在了解了原理之后,使用線程池時(shí)候也要格外小心,在阿里巴巴Java開發(fā)手冊中提到,禁止使用Executors直接創(chuàng)建線程池,因?yàn)榭赡軙?dǎo)致OOM(OutOfMemory ,內(nèi)存溢出),具體原因和替代方案參考:Java中線程池,你真的會用嗎?


          往期推薦

          Sublime Text 4 首個(gè)穩(wěn)定版終于來了:支持 GPU 渲染、兼容舊版本、Python API 升級


          帶爸媽去上海東方明珠旋轉(zhuǎn)餐廳體驗(yàn)了一把


          公司系統(tǒng)訪問量一大就崩,到點(diǎn)就爆,今天終于能夠解決了!



           

          直面Java第360期:如何使用樂觀鎖提升高并發(fā)的吞吐率并且不會超

          深入并發(fā)第015期:多線程代碼如何Debug?


          如果你喜歡本文,
          請長按二維碼,關(guān)注 Hollis.
          轉(zhuǎn)發(fā)至朋友圈,是對我最大的支持。

          點(diǎn)個(gè) 在看 
          喜歡是一種感覺
          在看是一種支持
          ↘↘↘
          瀏覽 46
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  中文字幕无码在线 | 黄色高潮喷水网站 | 久久综合狼人 | 小早川怜子一区二区三区88Av | 一区二区三区在线观看 |