溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

java多線程該設(shè)置多少個線程

發(fā)布時間:2021-11-01 17:05:34 來源:億速云 閱讀:138 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“java多線程該設(shè)置多少個線程”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“java多線程該設(shè)置多少個線程”吧!

我們在使用線程池的時候,會有兩個疑問點:

  •  線程池的線程數(shù)量設(shè)置過多會導致線程競爭激烈

  •  如果線程數(shù)量設(shè)置過少的話,還會導致系統(tǒng)無法充分利用計算機資源

那么如何設(shè)置才不會影響系統(tǒng)性能呢?

其實線程池的設(shè)置是有方法的,不是憑借簡單的估算來決定的。今天我們就來看看究竟有哪些計算方法可以復用,線程池中各個參數(shù)之間又存在怎樣的關(guān)系呢? 本文咱們來慢慢聊。

線程池原理

開始優(yōu)化之前,我們先來看看線程池的實現(xiàn)原理,有助于你更好地理解后面的內(nèi)容。

在 HotSpot VM 的線程模型中,Java 線程被一對一映射為內(nèi)核線程。Java 在使用線程執(zhí)行程序時,需要創(chuàng)建一個內(nèi)核線程;當該 Java 線程被終止時,這個內(nèi)核線程也會被回收。因此 Java 線程的創(chuàng)建與銷毀將會消耗一定的計算機資源,從而增加系統(tǒng)的性能開銷。

除此之外,大量創(chuàng)建線程同樣會給系統(tǒng)帶來性能問題,因為內(nèi)存和 CPU 資源都將被線程搶占,如果處理不當,就會發(fā)生內(nèi)存溢出、CPU 使用率超負荷等問題。

為了解決上述兩類問題,Java 提供了線程池概念,對于頻繁創(chuàng)建線程的業(yè)務場景,線程池可以創(chuàng)建固定的線程數(shù)量,并且在操作系統(tǒng)底層,輕量級進程將會把這些線程映射到內(nèi)核。

線程池可以提高線程復用,又可以固定最大線程使用量,防止無限制地創(chuàng)建線程。

當程序提交一個任務需要一個線程時,會去線程池中查找是否有空閑的線程,若有,則直接使用線程池中的線程工作,若沒有,會去判斷當前已創(chuàng)建的線程數(shù)量是否超過最大線程數(shù)量,如未超過,則創(chuàng)建新線程,如已超過,則進行排隊等待或者直接拋出異常。

線程池框架 Executor

Java 最開始提供了 ThreadPool 實現(xiàn)了線程池,為了更好地實現(xiàn)用戶級的線程調(diào)度,更有效地幫助開發(fā)人員進行多線程開發(fā),Java 提供了一套 Executor 框架。

這個框架中包括了 ScheduledThreadPoolExecutor 和 ThreadPoolExecutor 兩個核心線程池。前者是用來定時執(zhí)行任務,后者是用來執(zhí)行被提交的任務。

鑒于這兩個線程池的核心原理是一樣的,下面我們就重點看看 ThreadPoolExecutor 類是如何實現(xiàn)線程池的。

Executors 實現(xiàn)了以下四種類型的 ThreadPoolExecutor:

java多線程該設(shè)置多少個線程

Executors 利用工廠模式實現(xiàn)的四種線程池,我們在使用的時候需要結(jié)合生產(chǎn)環(huán)境下的實際場景。

不過我不太推薦使用它們,因為選擇使用 Executors 提供的工廠類,將會忽略很多線程池的參數(shù)設(shè)置,工廠類一旦選擇設(shè)置默認參數(shù),就很容易導致無法調(diào)優(yōu)參數(shù)設(shè)置,從而產(chǎn)生性能問題或者資源浪費。

我建議你使用 ThreadPoolExecutor 自我定制一套線程池(阿里規(guī)范中也是建議不要使用Executors 創(chuàng)建線程池,建議使用ThreadPoolExecutor 來創(chuàng)建線程池)。

進入四種工廠類后,我們可以發(fā)現(xiàn)除了 newScheduledThreadPool 類,其它類均使用了 ThreadPoolExecutor 類進行實現(xiàn),

java多線程該設(shè)置多少個線程

java多線程該設(shè)置多少個線程

java多線程該設(shè)置多少個線程

java多線程該設(shè)置多少個線程

你可以通過以下代碼簡單看下該方法

java多線程該設(shè)置多少個線程

corePoolSize:線程池的核心線程數(shù)量

maximumPoolSize:線程池的最大線程數(shù)

keepAliveTime:當線程數(shù)大于核心線程數(shù)時,多余的空閑線程存活的最長時間

unit:時間單位

workQueue:任務隊列,用來儲存等待執(zhí)行任務的隊列

threadFactory:線程工廠,用來創(chuàng)建線程,一般默認即可

handler:拒絕策略,當提交的任務過多而不能及時處理時,我們可以定制策略來處理任務

java多線程該設(shè)置多少個線程

我們還可以通過下面這張圖來了解下線程池中各個參數(shù)的相互關(guān)系:

通過上圖,我們發(fā)現(xiàn)線程池有兩個線程數(shù)的設(shè)置,一個為核心線程數(shù),一個為最大線程數(shù)。在創(chuàng)建完線程池之后,默認情況下,線程池中并沒有任何線程,等到有任務來才創(chuàng)建線程去執(zhí)行任務。

但有一種情況排除在外,就是調(diào)用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法的話,可以提前創(chuàng)建等于核心線程數(shù)的線程數(shù)量,這種方式被稱為預熱,在搶購系統(tǒng)中就經(jīng)常被用到。

當創(chuàng)建的線程數(shù)等于 corePoolSize 時,提交的任務會被加入到設(shè)置的阻塞隊列中。當隊列滿了,會創(chuàng)建線程執(zhí)行任務,直到線程池中的數(shù)量等于 maximumPoolSize。

當線程數(shù)量已經(jīng)等于 maximumPoolSize 時, 新提交的任務無法加入到等待隊列,也無法創(chuàng)建非核心線程直接執(zhí)行,我們又沒有為線程池設(shè)置拒絕策略,這時線程池就會拋出 RejectedExecutionException 異常,即線程池拒絕接受這個任務。

當線程池中創(chuàng)建的線程數(shù)量超過設(shè)置的 corePoolSize,在某些線程處理完任務后,如果等待 keepAliveTime 時間后仍然沒有新的任務分配給它,那么這個線程將會被回收。線程池回收線程時,會對所謂的“核心線程”和“非核心線程”一視同仁,直到線程池中線程的數(shù)量等于設(shè)置的 corePoolSize 參數(shù),回收過程才會停止。

即使是 corePoolSize 線程,在一些非核心業(yè)務的線程池中,如果長時間地占用線程數(shù)量,也可能會影響到核心業(yè)務的線程池,這個時候就需要把沒有分配任務的線程回收掉。

我們可以通過 allowCoreThreadTimeOut 設(shè)置項要求線程池:將包括“核心線程”在內(nèi)的,沒有任務分配的所有線程,在等待 keepAliveTime 時間后全部回收掉。

我們可以通過下面這張圖來了解下線程池的線程分配流程:

java多線程該設(shè)置多少個線程

計算線程數(shù)量

了解完線程池的實現(xiàn)原理和框架,我們就可以動手實踐優(yōu)化線程池的設(shè)置了。

我們知道,環(huán)境具有多變性,設(shè)置一個絕對精準的線程數(shù)其實是不大可能的,但我們可以通過一些實際操作因素來計算出一個合理的線程數(shù),避免由于線程池設(shè)置不合理而導致的性能問題。下面我們就來看看具體的計算方法。

一般多線程執(zhí)行的任務類型可以分為 CPU 密集型和 I/O 密集型,根據(jù)不同的任務類型,我們計算線程數(shù)的方法也不一樣。

CPU 密集型任務

這種任務消耗的主要是 CPU 資源,可以將線程數(shù)設(shè)置為 N(CPU 核心數(shù))+1,比 CPU 核心數(shù)多出來的一個線程是為了防止線程偶發(fā)的缺頁中斷,或者其它原因?qū)е碌娜蝿諘和6鴰淼挠绊憽?/p>

一旦任務暫停,CPU 就會處于空閑狀態(tài),而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。

下面我們用一個例子來驗證下這個方法的可行性,通過觀察 CPU 密集型任務在不同線程數(shù)下的性能情況就可以得出結(jié)果,你可以點擊Github下載到本地運行測試:

public class CPUTypeTest implements Runnable {        // 整體執(zhí)行時間,包括在隊列中等待的時間         List<Long> wholeTimeList;         // 真正執(zhí)行時間         List<Long> runTimeList;           private long initStartTime = 0;         /**      * 構(gòu)造函數(shù)      * @param runTimeList      * @param wholeTimeList      */      public CPUTypeTest(List<Long> runTimeList, List<Long> wholeTimeList) {          initStartTime = System.currentTimeMillis();          this.runTimeList = runTimeList;         this.wholeTimeList = wholeTimeList;     }      /**      * 判斷素數(shù)      * @param number     * @return      */     public boolean isPrime(final int number) {         if (number <= 1)              return false;          for (int i = 2; i <= Math.sqrt(number); i++) {              if (number % i == 0)                  return false;          }          return true;      }      /**      * 計算素數(shù)      * @param number      * @return      */     public int countPrimes(final int lower, final int upper) {          int total = 0;          for (int i = lower; i <= upper; i++) {              if (isPrime(i))                  total++;          }          return total;      }      public void run() {          long start = System.currentTimeMillis();          countPrimes(1, 1000000);          long end = System.currentTimeMillis();           long wholeTime = end - initStartTime;          long runTime = end - start;          wholeTimeList.add(wholeTime);          runTimeList.add(runTime);          System.out.println(" 單個線程花費時間:" + (end - start));      }  }

測試代碼在 4 核 intel i5 CPU 機器上的運行時間變化如下:

java多線程該設(shè)置多少個線程

綜上可知:當線程數(shù)量太小,同一時間大量請求將被阻塞在線程隊列中排隊等待執(zhí)行線程,此時 CPU 沒有得到充分利用;當線程數(shù)量太大,被創(chuàng)建的執(zhí)行線程同時在爭取 CPU 資源,又會導致大量的上下文切換,從而增加線程的執(zhí)行時間,影響了整體執(zhí)行效率。通過測試可知,4~6 個線程數(shù)是最合適的。

I/O 密集型任務

這種任務應用起來,系統(tǒng)會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內(nèi)不會占用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。

這里我們還是通過一個例子來驗證下這個公式是否可以標準化:

public class IOTypeTest implements Runnable {         // 整體執(zhí)行時間,包括在隊列中等待的時間         Vector<Long> wholeTimeList;       // 真正執(zhí)行時間         Vector<Long> runTimeList;          private long initStartTime = 0;        /**       * 構(gòu)造函數(shù)        * @param runTimeList         * @param wholeTimeList        */        public IOTypeTest(Vector<Long> runTimeList, Vector<Long> wholeTimeList) {           initStartTime = System.currentTimeMillis();           this.runTimeList = runTimeList;           this.wholeTimeList = wholeTimeList;       }             /**       *IO 操作        * @param number       * @return         * @throws IOException         */        public void readAndWrite() throws IOException {           File sourceFile = new File("D:/test.txt");    // 創(chuàng)建輸入流      BufferedReader input = new BufferedReader(new FileReader(sourceFile));   // 讀取源文件, 寫入到新的文件     String line = null;   while((line = input.readLine()) != null){   //System.out.println(line);   }     // 關(guān)閉輸入輸出流    input.close();        }             public void run() {           long start = System.currentTimeMillis();            try {                 readAndWrite();           } catch (IOException e) {               // TODO Auto-generated catch block                e.printStackTrace();              }             long end = System.currentTimeMillis();             long wholeTime = end - initStartTime;             long runTime = end - start;           wholeTimeList.add(wholeTime);            runTimeList.add(runTime);             System.out.println(" 單個線程花費時間:" + (end - start));         }     }

備注:由于測試代碼讀取 2MB 大小的文件,涉及到大內(nèi)存,所以在運行之前,我們需要調(diào)整 JVM 的堆內(nèi)存空間:-Xms4g -Xmx4g,避免發(fā)生頻繁的 FullGC,影響測試結(jié)果。

java多線程該設(shè)置多少個線程

通過測試結(jié)果,我們可以看到每個線程所花費的時間。當線程數(shù)量在 8 時,線程平均執(zhí)行時間是最佳的,這個線程數(shù)量和我們的計算公式所得的結(jié)果就差不多。

看完以上兩種情況下的線程計算方法,你可能還想說,在平常的應用場景中,我們常常遇不到這兩種極端情況,那么碰上一些常規(guī)的業(yè)務操作,比如,通過一個線程池實現(xiàn)向用戶定時推送消息的業(yè)務,我們又該如何設(shè)置線程池的數(shù)量呢?

此時我們可以參考以下公式來計算線程數(shù):

java多線程該設(shè)置多少個線程

WT:線程等待時間

ST:線程時間運行時間

我們可以通過 JDK 自帶的工具 VisualVM 來查看 WT/ST 比例,以下例子是基于運行純 CPU 運算的例子,我們可以看到:

WT(線程等待時間)= 36788ms [線程運行總時間] - 36788ms[ST(線程時間運行時間)]= 0    線程數(shù) =N(CPU 核數(shù))*(1+ 0 [WT(線程等待時間)]/36788ms[ST(線程時間運行時間)])= N(CPU 核數(shù))

這跟我們之前通過 CPU 密集型的計算公式 N+1 所得出的結(jié)果差不多。

java多線程該設(shè)置多少個線程

綜合來看,我們可以根據(jù)自己的業(yè)務場景,從“N+1”和“2N”兩個公式中選出一個適合的,計算出一個大概的線程數(shù)量,之后通過實際壓測,逐漸往“增大線程數(shù)量”和“減小線程數(shù)量”這兩個方向調(diào)整,然后觀察整體的處理時間變化,最終確定一個具體的線程數(shù)量。

到此,相信大家對“java多線程該設(shè)置多少個線程”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI