自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

不聊原理,拿來即用的線程池聊聊

網(wǎng)絡(luò) 通信技術(shù)
“池化技術(shù)相比大家已經(jīng)屢見不鮮了,線程池、數(shù)據(jù)庫連接池、Http 連接池等等都是對這個思想的應(yīng)用。池化技術(shù)的思想主要是為了減少每次獲取資源的消耗,提高對資源的利用率。

本文轉(zhuǎn)載自微信公眾號「JavaGuide」 作者:Guide哥,轉(zhuǎn)載本文請聯(lián)系JavaGuide公眾號。

[[329762]]

大家好,我是 Guide 哥,一個三觀比主角還正的技術(shù)人。今天再來繼續(xù)聊聊線程池~

這篇文章篇幅在5000字左右,絕對是干貨。標(biāo)題稍微有點(diǎn)夸張,嘿嘿,實(shí)際都是自己使用線程池的時候總結(jié)的一些個人感覺比較重要的點(diǎn)。

線程池知識回顧

開始這篇文章之前還是簡單介紹一嘴線程池,之前寫的《新手也能看懂的線程池學(xué)習(xí)總結(jié)》這篇文章介紹的很詳細(xì)了。

為什么要使用線程池?

“池化技術(shù)相比大家已經(jīng)屢見不鮮了,線程池、數(shù)據(jù)庫連接池、Http 連接池等等都是對這個思想的應(yīng)用。池化技術(shù)的思想主要是為了減少每次獲取資源的消耗,提高對資源的利用率。

線程池提供了一種限制和管理資源(包括執(zhí)行一個任務(wù))。每個線程池還維護(hù)一些基本統(tǒng)計(jì)信息,例如已完成任務(wù)的數(shù)量。

這里借用《Java 并發(fā)編程的藝術(shù)》提到的來說一下使用線程池的好處:

  • 降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
  • 提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時,任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。

線程池在實(shí)際項(xiàng)目的使用場景

線程池一般用于執(zhí)行多個不相關(guān)聯(lián)的耗時任務(wù),沒有多線程的情況下,任務(wù)順序執(zhí)行,使用了線程池的話可讓多個不相關(guān)聯(lián)的任務(wù)同時執(zhí)行。

假設(shè)我們要執(zhí)行三個不相關(guān)的耗時任務(wù),Guide 畫圖給大家展示了使用線程池前后的區(qū)別。

注意:下面三個任務(wù)可能做的是同一件事情,也可能是不一樣的事情。

使用線程池前后對比

 

如何使用線程池?

一般是通過 ThreadPoolExecutor 的構(gòu)造函數(shù)來創(chuàng)建線程池,然后提交任務(wù)給線程池執(zhí)行就可以了。

ThreadPoolExecutor構(gòu)造函數(shù)如下:

  1. /** 
  2.    * 用給定的初始參數(shù)創(chuàng)建一個新的ThreadPoolExecutor。 
  3.    */ 
  4.   public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數(shù)量 
  5.                             int maximumPoolSize,//線程池的最大線程數(shù) 
  6.                             long keepAliveTime,//當(dāng)線程數(shù)大于核心線程數(shù)時,多余的空閑線程存活的最長時間 
  7.                             TimeUnit unit,//時間單位 
  8.                             BlockingQueue<Runnable> workQueue,//任務(wù)隊(duì)列,用來儲存等待執(zhí)行任務(wù)的隊(duì)列 
  9.                             ThreadFactory threadFactory,//線程工廠,用來創(chuàng)建線程,一般默認(rèn)即可 
  10.                             RejectedExecutionHandler handler//拒絕策略,當(dāng)提交的任務(wù)過多而不能及時處理時,我們可以定制策略來處理任務(wù) 
  11.                              ) { 
  12.       if (corePoolSize < 0 || 
  13.           maximumPoolSize <= 0 || 
  14.           maximumPoolSize < corePoolSize || 
  15.           keepAliveTime < 0) 
  16.           throw new IllegalArgumentException(); 
  17.       if (workQueue == null || threadFactory == null || handler == null
  18.           throw new NullPointerException(); 
  19.       this.corePoolSize = corePoolSize; 
  20.       this.maximumPoolSize = maximumPoolSize; 
  21.       this.workQueue = workQueue; 
  22.       this.keepAliveTime = unit.toNanos(keepAliveTime); 
  23.       this.threadFactory = threadFactory; 
  24.       this.handler = handler; 
  25.   } 

簡單演示一下如何使用線程池,更詳細(xì)的介紹,請看:《新手也能看懂的線程池學(xué)習(xí)總結(jié)》。

  1. private static final int CORE_POOL_SIZE = 5; 
  2.  private static final int MAX_POOL_SIZE = 10; 
  3.  private static final int QUEUE_CAPACITY = 100; 
  4.  private static final Long KEEP_ALIVE_TIME = 1L; 
  5.  
  6.  public static void main(String[] args) { 
  7.  
  8.      //使用阿里巴巴推薦的創(chuàng)建線程池的方式 
  9.      //通過ThreadPoolExecutor構(gòu)造函數(shù)自定義參數(shù)創(chuàng)建 
  10.      ThreadPoolExecutor executor = new ThreadPoolExecutor( 
  11.              CORE_POOL_SIZE, 
  12.              MAX_POOL_SIZE, 
  13.              KEEP_ALIVE_TIME, 
  14.              TimeUnit.SECONDS, 
  15.              new ArrayBlockingQueue<>(QUEUE_CAPACITY), 
  16.              new ThreadPoolExecutor.CallerRunsPolicy()); 
  17.  
  18.      for (int i = 0; i < 10; i++) { 
  19.          executor.execute(() -> { 
  20.              try { 
  21.                  Thread.sleep(2000); 
  22.              } catch (InterruptedException e) { 
  23.                  e.printStackTrace(); 
  24.              } 
  25.              System.out.println("CurrentThread name:" + Thread.currentThread().getName() + "date:" + Instant.now()); 
  26.          }); 
  27.      } 
  28.      //終止線程池 
  29.      executor.shutdown(); 
  30.      try { 
  31.          executor.awaitTermination(5, TimeUnit.SECONDS); 
  32.      } catch (InterruptedException e) { 
  33.          e.printStackTrace(); 
  34.      } 
  35.      System.out.println("Finished all threads"); 
  36.  } 

控制臺輸出:

  1. CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:31.639Z 
  2. CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:31.639Z 
  3. CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:31.636Z 
  4. CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:31.639Z 
  5. CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:31.639Z 
  6. CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:33.656Z 
  7. CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:33.656Z 
  8. CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:33.656Z 
  9. CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:33.656Z 
  10. CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:33.656Z 
  11. Finished all threads 

線程池最佳實(shí)踐

簡單總結(jié)一下我了解的使用線程池的時候應(yīng)該注意的東西,網(wǎng)上似乎還沒有專門寫這方面的文章。

因?yàn)?Guide 還比較菜,有補(bǔ)充\完善\錯誤的地方,可以在評論區(qū)告知或者在微信上與我交流。

1. 使用 ThreadPoolExecutor 的構(gòu)造函數(shù)聲明線程池

線程池必須手動通過 ThreadPoolExecutor 的構(gòu)造函數(shù)來聲明,避免使用Executors 類的newFixedThreadPool 和 newCachedThreadPool ,因?yàn)榭赡軙?OOM 的風(fēng)險。

“Executors 返回線程池對象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor :允許請求的隊(duì)列長度為Integer.MAX_VALUE,可能堆積大量的請求,從而導(dǎo)致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool :允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE,可能會創(chuàng)建大量線程,從而導(dǎo)致 OOM。

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

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

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

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

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

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

 

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

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

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

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

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

我們再來看一個真實(shí)的事故案例! (本案例來源自:《線程池運(yùn)用不當(dāng)?shù)囊淮尉€上事故》@https://club.perfma.com/article/646639 ,很精彩的一個案例)

案例代碼概覽

 

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

試想這樣一種極端情況:

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

 

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

4.別忘記給線程池命名

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

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

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

1).利用 guava 的 ThreadFactoryBuilder

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

2).自己實(shí)現(xiàn) ThreadFactor。

  1. import java.util.concurrent.Executors; 
  2. import java.util.concurrent.ThreadFactory; 
  3. import java.util.concurrent.atomic.AtomicInteger; 
  4. /** 
  5.  * 線程工廠,它設(shè)置線程名稱,有利于我們定位問題。 
  6.  */ 
  7. public final class NamingThreadFactory implements ThreadFactory { 
  8.  
  9.     private final AtomicInteger threadNum = new AtomicInteger(); 
  10.     private final ThreadFactory delegate; 
  11.     private final String name
  12.  
  13.     /** 
  14.      * 創(chuàng)建一個帶名字的線程池生產(chǎn)工廠 
  15.      */ 
  16.     public NamingThreadFactory(ThreadFactory delegate, String name) { 
  17.         this.delegate = delegate; 
  18.         this.name = name; // TODO consider uniquifying this 
  19.     } 
  20.  
  21.     @Override 
  22.     public Thread newThread(Runnable r) { 
  23.         Thread t = delegate.newThread(r); 
  24.         t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); 
  25.         return t; 
  26.     } 
  27.  

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

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

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

常規(guī)操作

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

“上下文切換:多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。當(dāng)一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。概括來說就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時間片切換到另一個任務(wù)之前會先保存自己的狀態(tài),以便下次再切換回這個任務(wù)時,可以再加載這個任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過程就是一次上下文切換。上下文切換通常是計(jì)算密集型的。也就是說,它需要相當(dāng)可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實(shí)上,可能是操作系統(tǒng)中時間消耗最大的操作。Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時間消耗非常少。

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

如果我們設(shè)置的線程池數(shù)量太小的話,如果同一時間有大量任務(wù)/請求需要處理,可能會導(dǎo)致大量的請求/任務(wù)在任務(wù)隊(duì)列中排隊(duì)等待執(zhí)行,甚至?xí)霈F(xiàn)任務(wù)隊(duì)列滿了之后任務(wù)/請求無法處理的情況,或者大量任務(wù)堆積在任務(wù)隊(duì)列導(dǎo)致 OOM。這樣很明顯是有問題的!CPU 根本沒有得到充分利用。

但是,如果我們設(shè)置線程數(shù)量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導(dǎo)致大量的上下文切換,從而增加線程的執(zhí)行時間,影響了整體執(zhí)行效率。

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

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

如何判斷是 CPU 密集任務(wù)還是 IO 密集任務(wù)?

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

美團(tuán)的騷操作

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

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

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

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

我在這篇《新手也能看懂的線程池學(xué)習(xí)總結(jié)》 中就說過這三個參數(shù)是 ThreadPoolExecutor最重要的參數(shù),它們基本決定了線程池對于任務(wù)的處理策略。

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

 

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

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

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

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

 

責(zé)任編輯:武曉燕 來源: JavaGuide
相關(guān)推薦

2012-05-15 02:18:31

Java線程池

2021-02-01 08:28:24

Linux線程池Linux系統(tǒng)

2022-08-29 09:06:43

hippo4j動態(tài)線程池

2022-05-01 21:49:06

Python

2021-08-31 15:53:48

Nuxt 開箱服務(wù)

2023-12-27 08:07:49

Golang協(xié)程池Ants

2021-12-02 06:58:01

中間頁中間層編程

2022-08-05 09:06:07

Python腳本代碼

2020-12-10 08:24:40

線程池線程方法

2009-07-22 09:39:18

CLR線程池

2024-06-04 07:52:04

2023-11-29 16:38:12

線程池阻塞隊(duì)列開發(fā)

2023-07-11 08:34:25

參數(shù)流程類型

2024-07-15 08:20:24

2021-07-16 11:35:20

Java線程池代碼

2025-01-10 00:00:10

2018-09-14 16:59:32

2022-09-06 08:31:09

線程池工具系統(tǒng)

2020-03-05 15:34:16

線程池C語言局域網(wǎng)

2018-10-31 15:54:47

Java線程池源碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號