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

          8 個線程池最佳實踐和坑!使用不當直接生產事故!!

          共 11678字,需瀏覽 24分鐘

           ·

          2024-07-20 13:07

          這篇文章我會簡單總結一下我了解的使用線程池的時候應該注意的坑以及一些優(yōu)秀的實踐。拿來即用,美滋滋!

          內容概覽:

          圖片

          1、正確聲明線程池

          線程池必須手動通過 ThreadPoolExecutor 的構造函數(shù)來聲明,避免使用Executors 類創(chuàng)建線程池,會有 OOM 風險。

          Executors 返回線程池對象的弊端如下(后文會詳細介紹到):

          • FixedThreadPoolSingleThreadExecutor :使用的是無界的 LinkedBlockingQueue,任務隊列最大長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
          • CachedThreadPool :使用的是同步隊列 SynchronousQueue, 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會創(chuàng)建大量線程,從而導致 OOM。
          • ScheduledThreadPoolSingleThreadScheduledExecutor : 使用的無界的延遲阻塞隊列DelayedWorkQueue,任務隊列最大長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。

          說白了就是:使用有界隊列,控制線程創(chuàng)建數(shù)量。

          除了避免 OOM 的原因之外,不推薦使用 Executors提供的兩種快捷的線程池的原因還有:

          • 實際使用中需要根據(jù)自己機器的性能、業(yè)務場景來手動配置線程池的參數(shù)比如核心線程數(shù)、使用的任務隊列、飽和策略等等。
          • 我們應該顯示地給我們的線程池命名,這樣有助于我們定位問題。

          2、監(jiān)測線程池運行狀態(tài)

          你可以通過一些手段來檢測線程池的運行狀態(tài)比如 SpringBoot 中的 Actuator 組件。

          除此之外,我們還可以利用 ThreadPoolExecutor 的相關 API 做一個簡陋的監(jiān)控。從下圖可以看出, ThreadPoolExecutor提供了獲取線程池當前的線程數(shù)和活躍線程數(shù)、已經(jīng)執(zhí)行完成的任務數(shù)、正在排隊中的任務數(shù)等等。

          圖片

          下面是一個簡單的 Demo。printThreadPoolStatus()會每隔一秒打印出線程池的線程數(shù)、活躍線程數(shù)、完成的任務數(shù)、以及隊列中的任務數(shù)。

          /**
           * 打印線程池的狀態(tài)
           *
           * @param threadPool 線程池對象
           */
          public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
              ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status"false));
              scheduledExecutorService.scheduleAtFixedRate(() -> {
                  log.info("=========================");
                  log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
                  log.info("Active Threads: {}", threadPool.getActiveCount());
                  log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
                  log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
                  log.info("=========================");
              }, 0, 1, TimeUnit.SECONDS);
          }

          3、建議不同類別的業(yè)務用不同的線程池

          很多人在實際項目中都會有類似這樣的問題:我的項目中多個業(yè)務需要用到線程池,是為每個線程池都定義一個還是說定義一個公共的線程池呢?

          一般建議是不同的業(yè)務使用不同的線程池,配置線程池的時候根據(jù)當前業(yè)務的情況對當前線程池進行配置,因為不同的業(yè)務的并發(fā)以及對資源的使用情況都不同,重心優(yōu)化系統(tǒng)性能瓶頸相關的業(yè)務。

          我們再來看一個真實的事故案例! (本案例來源自:《線程池運用不當?shù)囊淮尉€上事故》[1] ,很精彩的一個案例)

          案例代碼概覽

          上面的代碼可能會存在死鎖的情況,為什么呢?畫個圖給大家捋一捋。

          試想這樣一種極端情況:假如我們線程池的核心線程數(shù)為 n,父任務(扣費任務)數(shù)量為 n,父任務下面有兩個子任務(扣費任務下的子任務),其中一個已經(jīng)執(zhí)行完成,另外一個被放在了任務隊列中。由于父任務把線程池核心線程資源用完,所以子任務因為無法獲取到線程資源無法正常執(zhí)行,一直被阻塞在隊列中。父任務等待子任務執(zhí)行完成,而子任務等待父任務釋放線程池資源,這也就造成了 "死鎖"

          線程池使用不當導致死鎖

          解決方法也很簡單,就是新增加一個用于執(zhí)行子任務的線程池專門為其服務。

          4、別忘記給線程池命名

          初始化線程池的時候需要顯示命名(設置線程池名稱前綴),有利于定位問題。

          默認情況下創(chuàng)建的線程名字類似 pool-1-thread-n 這樣的,沒有業(yè)務含義,不利于我們定位問題。

          給線程池里的線程命名通常有下面兩種方式:

          1、利用 guava 的 ThreadFactoryBuilder

          ThreadFactory threadFactory = new ThreadFactoryBuilder()
                                  .setNameFormat(threadNamePrefix + "-%d")
                                  .setDaemon(true).build();
          ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory)

          2、自己實現(xiàn) ThreadFactor

          import java.util.concurrent.Executors;
          import java.util.concurrent.ThreadFactory;
          import java.util.concurrent.atomic.AtomicInteger;
          /**
           * 線程工廠,它設置線程名稱,有利于我們定位問題。
           */
          public final class NamingThreadFactory implements ThreadFactory {

              private final AtomicInteger threadNum = new AtomicInteger();
              private final ThreadFactory delegate;
              private final String name;

              /**
               * 創(chuàng)建一個帶名字的線程池生產工廠
               */
              public NamingThreadFactory(ThreadFactory delegate, String name) {
                  this.delegate = delegate;
                  this.name = name; // TODO consider uniquifying this
              }

              @Override
              public Thread newThread(Runnable r) {
                  Thread t = delegate.newThread(r);
                  t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
                  return t;
              }

          }

          5、正確配置線程池參數(shù)

          說到如何給線程池配置參數(shù),美團的騷操作至今讓我難忘(后面會提到)!

          我們先來看一下各種書籍和博客上一般推薦的配置線程池參數(shù)的方式,可以作為參考!

          常規(guī)操作

          很多人甚至可能都會覺得把線程池配置過大一點比較好!我覺得這明顯是有問題的。就拿我們生活中非常常見的一例子來說:并不是人多就能把事情做好,增加了溝通交流成本。你本來一件事情只需要 3 個人做,你硬是拉來了 6 個人,會提升做事效率嘛?我想并不會。 線程數(shù)量過多的影響也是和我們分配多少人做事情一樣,對于多線程這個場景來說主要是增加了上下文切換成本。不清楚什么是上下文切換的話,可以看我下面的介紹。

          上下文切換:

          多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。概括來說就是:當前任務在執(zhí)行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態(tài),以便下次再切換回這個任務時,可以再加載這個任務的狀態(tài)。任務從保存到再加載的過程就是一次上下文切換

          上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統(tǒng)中時間消耗最大的操作。

          Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

          類比于實現(xiàn)世界中的人類通過合作做某件事情,我們可以肯定的一點是線程池大小設置過大或者過小都會有問題,合適的才是最好。

          • 如果我們設置的線程池數(shù)量太小的話,如果同一時間有大量任務/請求需要處理,可能會導致大量的請求/任務在任務隊列中排隊等待執(zhí)行,甚至會出現(xiàn)任務隊列滿了之后任務/請求無法處理的情況,或者大量任務堆積在任務隊列導致 OOM。這樣很明顯是有問題的,CPU 根本沒有得到充分利用。
          • 如果我們設置線程數(shù)量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執(zhí)行時間,影響了整體執(zhí)行效率。

          有一個簡單并且適用面比較廣的公式:

          • CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數(shù)設置為 N(CPU 核心數(shù))+1。比 CPU 核心數(shù)多出來的一個線程是為了防止線程偶發(fā)的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處于空閑狀態(tài),而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。
          • I/O 密集型任務(2N): 這種任務應用起來,系統(tǒng)會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會占用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。

          如何判斷是 CPU 密集任務還是 IO 密集任務?

          CPU 密集型簡單理解就是利用 CPU 計算能力的任務比如你在內存中對大量數(shù)據(jù)進行排序。但凡涉及到網(wǎng)絡讀取,文件讀取這類都是 IO 密集型,這類任務的特點是 CPU 計算耗費時間相比于等待 IO 操作完成的時間來說很少,大部分時間都花在了等待 IO 操作完成上。

          ?? 拓展一下:

          線程數(shù)更嚴謹?shù)挠嬎愕姆椒☉撌牵?code style="color: rgb(40, 202, 113);font-size: 14px;line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;">最佳線程數(shù) = N(CPU 核心數(shù))?(1+WT(線程等待時間)/ST(線程計算時間)),其中 WT(線程等待時間)=線程運行總時間 - ST(線程計算時間)

          線程等待時間所占比例越高,需要越多線程。線程計算時間所占比例越高,需要越少線程。

          我們可以通過 JDK 自帶的工具 VisualVM 來查看 WT/ST 比例。

          CPU 密集型任務的 WT/ST 接近或者等于 0,因此, 線程數(shù)可以設置為 N(CPU 核心數(shù))?(1+0)= N,和我們上面說的 N(CPU 核心數(shù))+1 差不多。

          IO 密集型任務下,幾乎全是線程等待時間,從理論上來說,你就可以將線程數(shù)設置為 2N(按道理來說,WT/ST 的結果應該比較大,這里選擇 2N 的原因應該是為了避免創(chuàng)建過多線程吧)。

          公示也只是參考,具體還是要根據(jù)項目實際線上運行情況來動態(tài)調整。我在后面介紹的美團的線程池參數(shù)動態(tài)配置這種方案就非常不錯,很實用!

          美團的騷操作

          美團技術團隊在《Java 線程池實現(xiàn)原理及其在美團業(yè)務中的實踐》[3]這篇文章中介紹到對線程池參數(shù)實現(xiàn)可自定義配置的思路和方法。

          美團技術團隊的思路是主要對線程池的核心參數(shù)實現(xiàn)自定義可配置。這三個核心參數(shù)是:

          • corePoolSize : 核心線程數(shù)線程數(shù)定義了最小可以同時運行的線程數(shù)量。
          • maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數(shù)量變?yōu)樽畲缶€程數(shù)。
          • workQueue: 當新任務來的時候會先判斷當前運行的線程數(shù)量是否達到核心線程數(shù),如果達到的話,新任務就會被存放在隊列中。

          為什么是這三個參數(shù)?

          如何支持參數(shù)動態(tài)配置? 且看 ThreadPoolExecutor 提供的下面這些方法。

          圖片

          格外需要注意的是corePoolSize, 程序運行期間的時候,我們調用 setCorePoolSize()這個方法的話,線程池會首先判斷當前工作線程數(shù)是否大于corePoolSize,如果大于的話就會回收工作線程。

          另外,你也看到了上面并沒有動態(tài)指定隊列長度的方法,美團的方式是自定義了一個叫做 ResizableCapacityLinkedBlockIngQueue 的隊列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 關鍵字修飾給去掉了,讓它變?yōu)榭勺兊模?/p>

          最終實現(xiàn)的可動態(tài)修改線程池參數(shù)效果如下。??????

          動態(tài)配置線程池參數(shù)最終效果

          如果我們的項目也想要實現(xiàn)這種效果的話,可以借助現(xiàn)成的開源項目:

          • Hippo-4[4] :一款強大的動態(tài)線程池框架,解決了傳統(tǒng)線程池使用存在的一些痛點比如線程池參數(shù)沒辦法動態(tài)修改、不支持運行時變量的傳遞、無法執(zhí)行優(yōu)雅關閉。除了支持動態(tài)修改線程池參數(shù)、線程池任務傳遞上下文,還支持通知報警、運行監(jiān)控等開箱即用的功能。
          • Dynamic TP[5] :輕量級動態(tài)線程池,內置監(jiān)控告警功能,集成三方中間件線程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通過 SPI 自定義實現(xiàn))。

          6、線程池使用的一些小坑

          重復創(chuàng)建線程池的坑

          線程池是可以復用的,一定不要頻繁創(chuàng)建線程池比如一個用戶請求到了就單獨創(chuàng)建一個線程池。

          @GetMapping("wrong")
          public String wrong() throws InterruptedException {
              // 自定義線程池
              ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy());

              //  處理任務
              executor.execute(() -> {
                // ......
              }
              return "OK";
          }

          出現(xiàn)這種問題的原因還是對于線程池認識不夠,需要加強線程池的基礎知識。

          Spring 內部線程池的坑

          使用 Spring 內部線程池時,一定要手動自定義線程池,配置合理的參數(shù),不然會出現(xiàn)生產問題(一個請求創(chuàng)建一個線程)。

          @Configuration
          @EnableAsync
          public class ThreadPoolExecutorConfig {

              @Bean(name="threadPoolExecutor")
              public Executor threadPoolExecutor(){
                  ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
                  int processNum = Runtime.getRuntime().availableProcessors(); // 返回可用處理器的Java虛擬機的數(shù)量
                  int corePoolSize = (int) (processNum / (1 - 0.2));
                  int maxPoolSize = (int) (processNum / (1 - 0.5));
                  threadPoolExecutor.setCorePoolSize(corePoolSize); // 核心池大小
                  threadPoolExecutor.setMaxPoolSize(maxPoolSize); // 最大線程數(shù)
                  threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 隊列程度
                  threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY);
                  threadPoolExecutor.setDaemon(false);
                  threadPoolExecutor.setKeepAliveSeconds(300);// 線程空閑時間
                  threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // 線程名字前綴
                  return threadPoolExecutor;
              }
          }

          線程池和 ThreadLocal 共用的坑

          線程池和 ThreadLocal共用,可能會導致線程從ThreadLocal獲取到的是舊值/臟數(shù)據(jù)。這是因為線程池會復用線程對象,與線程對象綁定的類的靜態(tài)屬性 ThreadLocal 變量也會被重用,這就導致一個線程可能獲取到其他線程的ThreadLocal 值。

          不要以為代碼中沒有顯示使用線程池就不存在線程池了,像常用的 Web 服務器 Tomcat 處理任務為了提高并發(fā)量,就使用到了線程池,并且使用的是基于原生 Java 線程池改進完善得到的自定義線程池。

          當然了,你可以將 Tomcat 設置為單線程處理任務。不過,這并不合適,會嚴重影響其處理任務的速度。

          server.tomcat.max-threads=1

          解決上述問題比較建議的辦法是使用阿里巴巴開源的 TransmittableThreadLocal(TTL)。TransmittableThreadLocal類繼承并加強了 JDK 內置的InheritableThreadLocal類,在使用線程池等會池化復用線程的執(zhí)行組件情況下,提供ThreadLocal值的傳遞功能,解決異步執(zhí)行時上下文傳遞的問題。

          InheritableThreadLocal 項目地址:https://github.com/alibaba/transmittable-thread-local 


          程序汪接私活項目目錄,2023年總結

          Java項目分享  最新整理全集,找項目不累啦 07版

          程序汪10萬接的無線共享充電寶項目,開發(fā)周期3個月

          程序汪1萬接的企業(yè)官網(wǎng)項目,開發(fā)周期15天

          程序汪8萬接的共享口罩項目,開發(fā)周期1個月

          程序汪8萬塊的飲水機物聯(lián)網(wǎng)私活項目經(jīng)驗分享

          程序汪接的4萬智慧餐飲項目

          程序汪接的酒店在線開房項目,另外一個好聽的名字叫智慧酒店


          歡迎添加程序汪個人微信 itwang008  進粉絲群或圍觀朋友圈

          瀏覽 278
          1點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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片免费看 | 一级片在线免费黄 | 青娱乐青青草视频在线观看 | 99精品在线观看免费视频 |