日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

调整线程池的重要性

發布時間:2023/12/3 编程问答 55 豆豆
生活随笔 收集整理的這篇文章主要介紹了 调整线程池的重要性 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

無論您是否知道,您的Java Web應用程序很可能都使用線程池來處理傳入的請求。 這是許多人忽略的實現細節,但是遲早您需要了解如何使用該池以及如何為您的應用程序正確調整池。 本文旨在說明線程模型,什么是線程池以及正確配置線程池所需執行的操作。

單螺紋

讓我們從一些基礎知識開始,并隨著線程模型的發展而前進。 無論您使用哪種應用程序服務器或框架, Tomcat , Dropwizard , Jetty ,它們都使用相同的基本方法。 一個深埋在Web服務器內部的套接字。 該套接字正在偵聽傳入的TCP連接,并接受它們。 一旦被接受,就可以從新建立的TCP連接中讀取數據,進行解析并將其轉換為HTTP請求。 然后將此請求移交給Web應用程序,以完成其所需的操作。

為了理解線程的作用,我們將不使用應用程序服務器,而是從頭開始構建一個簡單的服務器。 該服務器反映了大多數應用程序服務器的功能。 首先,單線程Web服務器可能看起來像這樣:

ServerSocket listener = new ServerSocket(8080); try {while (true) {Socket socket = listener.accept();try {handleRequest(socket);} catch (IOException e) {e.printStackTrace();}} } finally {listener.close(); }

此代碼在端口8080上創建一個ServerSocket ,然后在一個緊密循環中,ServerSocket檢查是否接受新連接。 一旦接受,套接字將傳遞給handleRequest方法。 該方法通常將讀取HTTP請求,執行所需的任何過程,然后編寫響應。 在此簡單示例中,handleRequest讀取一行,并返回簡短的HTTP響應。 handleRequest做一些更復雜的事情是正常的,例如從數據庫中讀取或進行某種其他類型的IO。

final static String response =“HTTP/1.0 200 OK\r\n” +“Content-type: text/plain\r\n” +“\r\n” +“Hello World\r\n”;public static void handleRequest(Socket socket) throws IOException {// Read the input stream, and return “200 OK”try {BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));log.info(in.readLine());OutputStream out = socket.getOutputStream();out.write(response.getBytes(StandardCharsets.UTF_8));} finally {socket.close();} }

由于只有一個線程處理所有接受的套接字,因此在接受下一個請求之前,必須完全處理每個請求。 在實際的應用程序中,等效的handleRequest方法返回大約100毫秒的時間可能是正常的。 如果是這種情況,服務器將被限制為每秒僅處理10個請求,一個接一個。

多線程

即使handleRequest可能在IO上被阻止,CPU也可以自由處理更多請求。 使用單線程方法是不可能的。 因此,可以通過創建多個線程來改進此服務器以允許并發操作:

public static class HandleRequestRunnable implements Runnable {final Socket socket;public HandleRequestRunnable(Socket socket) {this.socket = socket;}public void run() {try {handleRequest(socket);} catch (IOException e) {e.printStackTrace();}} }ServerSocket listener = new ServerSocket(8080); try {while (true) {Socket socket = listener.accept();new Thread(new HandleRequestRunnable(socket)).start();} } finally {listener.close(); }

在這里,仍然在單個線程內的緊密循環中調用accept(),但是一旦接受TCP連接并且有可用的套接字,就會產生一個新線程。 這個產生的線程執行一個HandleRequestRunnable,它從上面簡單地調用相同的handleRequest方法。

創建新線程后,現在可以釋放原始的accept()線程來處理更多的TCP連接,并允許應用程序同時處理請求。 該技術被稱為“每個請求線程”,是最流行的方法。 值得注意的是,還有其他方法,例如事件驅動的異步模型NGINX和Node.js部署,但是它們不使用線程池,因此不在本文討論范圍之內。

在“每個請求的線程”方法中,創建新線程(然后銷毀它)可能會很昂貴,因為JVM和OS都需要分配資源。 另外,在上述實現中,正在創建的線程數不受限制。 不受限制是很成問題的,因為它會很快導致資源枯竭。

資源枯竭

每個線程都需要一定數量的內存用于堆棧。 在最新的64位JVM上, 默認堆棧大小為1024KB。 如果服務器收到大量請求,或者handleRequest方法變慢,則服務器可能會出現大量并發線程。 因此,要管理1000個并發請求,僅用于線程堆棧的1000個線程將消耗1GB的JVM RAM。 另外,在每個線程中執行的代碼將在處理請求所需的堆上創建對象。 這非常Swift地加起來,并且可能超過分配給JVM的堆空間,從而對垃圾收集器施加壓力,從而導致崩潰并最終導致OutOfMemoryErrors 。

線程不僅消耗RAM,而且可能使用其他有限資源,例如文件句柄或數據庫連接。 超過這些可能導致其他類型的錯誤或崩潰。 因此,為了避免耗盡資源,重要的是避免無限制的數據結構。

不是靈丹妙藥,但是可以通過使用-Xss標志調整堆棧大小來緩解堆棧大小問題。 較小的堆棧將減少每個線程的開銷,但可能導致StackOverflowErrors 。 您的里程會有所不同,但是對于許多應用程序,默認的1024KB過多,因此較小的256KB或512KB值可能更合適。 Java允許的最小值是16KB。

線程池

為了避免連續創建新線程并限制最大數量,可以使用簡單的線程池。 簡而言之,該池跟蹤所有線程,在需要達到上限時創建新線程,并在可能的情況下重用空閑線程。

ServerSocket listener = new ServerSocket(8080); ExecutorService executor = Executors.newFixedThreadPool(4); try {while (true) {Socket socket = listener.accept();executor.submit( new HandleRequestRunnable(socket) );} } finally {listener.close(); }

現在,此代碼不是直接創建線程,而是使用ExecutorService,該服務提交要在線程池中執行的工作(用Runnables術語)。 在此示例中,四個線程的固定線程池用于處理所有傳入的請求。 這限制了“進行中”請求的數量,因此限制了資源的使用。

除了newFixedThreadPool之外 ,Executors實用程序類還提供了newCachedThreadPool方法。 這受到較早的無限線程數量的困擾,但是只要有可能,就利用先前創建但現在空閑的線程。 通常,這種類型的池對于不會阻塞外部資源的短暫請求很有用。

ThreadPoolExecutors可以直接構造,從而可以自定義其行為。 例如,可以定義池中線程的最小和最大數量,以及何時創建和銷毀線程的策略。 簡短的示例。

工作隊列

在固定線程池的情況下,細心的讀者可能想知道如果所有線程都忙,并且有新請求進入,會發生什么情況。那么ThreadPoolExecutor使用隊列來容納線程可用之前的待處理請求。 默認情況下,Executors.newFixedThreadPool和Executors.newCachedThreadPool都使用無界LinkedList。 再次,這會導致資源耗盡問題,盡管速度要慢得多,因為每個排隊的請求都小于完整線程,并且通常不會使用那么多資源。 但是,在我們的示例中,每個排隊的請求都持有一個套接字(取決于OS)將占用一個文件句柄。 這是操作系統將限制的資源類型,因此除非有必要,否則最好不要保留它。 因此,限制工作隊列的大小也很有意義。

public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(capacity),new ThreadPoolExecutor.DiscardPolicy()); }public static void boundedThreadPoolServerSocket() throws IOException {ServerSocket listener = new ServerSocket(8080);ExecutorService executor = newBoundedFixedThreadPool(4, 16);try {while (true) {Socket socket = listener.accept();executor.submit( new HandleRequestRunnable(socket) );}} finally {listener.close();} }

同樣,我們創建了一個線程池,但是我們沒有使用Executors.newFixedThreadPool幫助器方法,而是自己創建了ThreadPoolExecutor,并傳遞了一個限制為16個元素的有界LinkedBlockingQueue 。 或者,可以使用ArrayBlockingQueue ,它是有界緩沖區的實現。

如果所有線程都忙,并且隊列已滿,則下一步將由ThreadPoolExecutor的最后一個參數定義。 在此示例中,使用了DiscardPolicy ,它僅丟棄將使隊列溢出的所有工作。 還有其他政策,如AbortPolicy它拋出一個異常,或CallerRunsPolicy執行該調用者的線程上的工作。 該CallerRunsPolicy提供了一種簡單的方法來自我限制可以添加作業的速率,但是,這可能是有害的,阻塞了一個應保持暢通的線程。

一個好的默認策略是“放棄”或“中止”,這兩者都會放棄工作。 在這些情況下,很容易向客戶端返回一個簡單的錯誤,例如HTTP 503“服務不可用” 。 有人會爭辯說只是增加隊列大小,然后所有工作最終都會運行。 但是,用戶不愿永遠等待,如果從根本上說工作的執行速度超過了可以執行的速度,那么隊列將無限期地增長。 相反,該隊列僅應用于消除突發請求,或處理處理中的短暫停頓。 在正常操作中,隊列應為空。

有多少個線程?

現在我們了解了如何創建線程池,困難的問題是應該有多少個線程可用? 我們確定最大數量應該限制為不導致資源耗盡。 這包括所有類型的資源,內存(堆棧和堆),打開的文件句柄,打開的TCP連接,遠程數據庫可以處理的連接數以及任何其他有限資源。 相反,如果線程是CPU綁定的,而不是IO綁定的,則應將物理核心的數量視為有限,并且每個核心最多只能創建一個線程。

這一切都取決于應用程序正在做的工作。 用戶應使用各種池大小和實際的請求混合來運行負載測試。 每次增加它們的線程池大小直到斷點。 這樣就可以找到資源耗盡時的上限。 在某些情況下,明智的做法是增加可用資源的數量,例如為JVM提供更多的RAM,或調整OS以允許更多的文件句柄。 但是,在某個時候將達到理論上限,應該注意,但這還不是故事的結局。

利特爾定律

排隊論,尤其是利特爾定律 ,可以用來幫助理解線程池的屬性。 簡單來說,利特爾定律描述了三個變量之間的關系。 L進行中的請求數量,λ新請求到達的速率,W平均處理請求的時間。 例如,如果每秒有10個請求到達,并且每個請求花費一秒鐘的時間來處理,則在任何時間平均有10個正在進行的請求。 在我們的示例中,這映射為使用10個線程。 如果處理單個請求的時間增加了一倍,則運行中的平均請求數也將增加一倍,達到20,因此需要20個線程。

了解執行時間對進行中的請求的影響非常重要。 某些后端資源(例如數據庫)停頓是很常見的,導致請求花費更長的時間來處理,從而很快耗盡了線程池。 因此,理論上限可能不是池大小的適當限制。 相反,應該對執行時間設置一個限制,并與理論上限結合使用。

例如,假設在JVM超過其內存分配之前,可以處理的最大傳輸中請求為1000。 如果我們預算每個請求的時間不超過30秒,那么在最壞的情況下,我們應該期望每秒處理不超過33個請求。 但是,如果一切正常,并且請求僅用500毫秒即可處理,則應用程序每秒只能在1000個線程上處理2000個請求。 指定可以使用隊列來消除短暫的延遲突發也可能是合理的。

為什么要麻煩?

如果線程池中的線程太少,則存在以下風險:資源利用不足,不必要地將用戶拒之門外。 但是,如果允許太多線程,則會發生資源耗盡,這可能會造成更大的破壞。

不僅會耗盡本地資源,還可能對其他資源產生不利影響。 例如,多個應用程序查詢同一個后端數據庫。 數據庫通常對并發連接數有硬性限制。 如果一個行為異常的應用程序消耗了所有這些連接,它將阻止其他應用程序訪問數據庫。 造成大范圍的中斷。

更糟糕的是,可能會發生級聯故障。 想象一下一個環境,其中有一個應用程序的多個實例,位于一個公共負載均衡器的后面。 如果實例之一由于正在進行的請求過多而開始用盡內存,那么JVM將花費更多時間進行垃圾收集,并減少處理請求的時間。 這種減慢速度將降低該實例的容量,并迫使其他實例處理更高比例的傳入請求。 隨著他們現在使用其無限制線程池處理更多請求,會發生相同的問題。 它們耗盡了內存,然后再次開始積極地進行垃圾回收。 這個惡性循環在所有實例之間級聯,直到出現系統性故障。

我經常觀察到沒有進行負載測試,并且允許任意數量的線程。 在通常情況下,應用程序可以使用少量線程以傳入速率愉快地處理請求。 但是,如果處理請求取決于遠程服務,并且該服務暫時變慢,則W的增加(平均處理時間)的影響會很快耗盡池。 由于從未對應用程序進行最大數量的負載測試,因此會出現之前概述的所有資源耗盡問題。

有多少個線程池?

在微 服務或面向服務的體系結構 (SOA)中,訪問多個遠程后端服務是正常的。 此設置特別容易發生故障,因此應仔細解決這些問題。 如果遠程服務的性能下降,則可能導致線程池快速達到其極限,從而丟棄后續請求。 但是,并非所有請求都可能需要此不正常的后端,但是由于線程池已滿,因此不必要地刪除了這些請求。

通過提供特定于后端的線程池,可以隔離每個后端的故障。 在這種模式下,仍然只有一個請求工作程序池,但是如果請求需要調用遠程服務,則工作將轉移到該后端的線程池。 這使主請求池不會受到單個緩慢后端的負擔。 這樣,只有需要特定后端池的請求才會在故障時受到影響。

多個線程池的最后一個好處是,它有助于避免某種形式的死鎖。 如果由于尚未處理的請求而導致每個可用線程都被阻塞,則將發生死鎖,并且沒有線程可以前進。 當使用多個池并充分了解它們執行的工作時,可以在某種程度上緩解此問題。

截止日期和其他最佳做法

常見的最佳做法是確保所有遠程呼叫都有最后期限。 也就是說,如果遠程服務在合理時間內沒有響應,則該請求將被放棄。 可以在線程池中使用相同的技術。 具體來說,如果線程正在處理一個請求的時間超過了定義的期限,則應終止該線程。 為新請求騰出空間,并在W上設置上限。這似乎是一種浪費,但是如果用戶(通常是Web瀏覽器)正在等待響應,則30秒后,瀏覽器可能只會給出無論如何,還是用戶可能會變得急躁并離開。

快速失敗是在為后端創建池時可以采用的另一種方法。 如果后端發生故障,則線程池將Swift填充等待連接到無響應后端的請求。 相反,可以將后端標記為不正常,所有后續請求都可能立即失敗,而不是不必要地等待。 但是請注意,需要一種機制來確定后端何時再次恢復健康。

最后,如果一個請求需要獨立地調用多個后端,則應該可以并行而不是順序地調用它們。 這將減少等待時間,但以增加線程為代價。

幸運的是,有一個很棒的庫hystrix ,它打包了許多這些最佳實踐,并以簡單安全的方式公開了它們。

結論

希望本文能增進您對線程池的了解。 通過了解應用程序的需求,并結合使用最大線程數和平均響應時間,可以確定適當的線程池。 這不僅可以避免級聯故障,而且可以幫助計劃和配置您的服務。

即使您的應用程序可能未顯式使用線程池,但它們還是被應用程序服務器或更高級別的抽象隱式使用。 Tomcat , JBoss , Undertow , Dropwizard都為其線程池(執行servlet的池)提供了多個可調參數。

翻譯自: https://www.javacodegeeks.com/2015/12/importance-tuning-thread-pools.html

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的调整线程池的重要性的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。