文章編號:11568時間:2024-10-01人氣:
Tomcat 是一個免費且開源的 Java Servlet 容器,可用于部署 Java EE Web 應用程序。Tomcat 7.0 是一個長期支持(LTS)版本,自 2011 年以來一直受到支持,并且仍然被廣泛使用。
獲取 Tomcat 7.0 的最簡單方法是直接從 Apache Tomcat 網站下載。以下是下載步驟:
Tomcat 使用線程池來處理傳入的請求。線程池是由 ExecutorService 對象表示的。要獲得 Tomcat 線程池對象,請執行以下步驟:
import org.apache.catalina.connector.Connector;
Connector connector = (Connector) context.getConnector();
ExecutorService executorService = connector.getExecutorService();
現在,您擁有了 Tomcat 線程池對象,可以使用它來控制線程池的行為,例如調整線程數、任務隊列大小等。
獲取 Tomcat 7.0 并獲取其線程池對象是一個快速且簡單的過程。通過遵循這些步驟,您可以輕松地設置 Tomcat 環境并開始部署 Java EE Web 應用程序。
在程序中,我們會用各種池化技術來緩存創建昂貴的對象,比如線程池、連接池、內存池。 一般是預先創建一些對象放入池中,使用的時候直接取出使用,用完歸還以便復用,還會通過一定的策略調整池中緩存對象的數量,實現池的動態伸縮。
由于線程的創建比較昂貴,隨意、沒有控制地創建大量線程會造成性能問題,因此短平快的任務一般考慮使用線程池來處理,而不是直接創建線程。 但是在使用線程池的時候應該注意線程池的使用,如果使用不當,將會導致生產事故。
一、線程池的聲明需要手動進行Java 中的 Executors 類定義了一些快捷的工具方法,來幫助我們快速創建線程池。 《阿里巴巴 Java 開發手冊》中提到,禁止使用這些方法來創建線程池,而應該手動 new ThreadPoolExecutor 來創建線程池。 這一條規則的背后,是大量血淋淋的生產事故,最典型的就是 newFixedThreadPool 和 newCachedThreadPool,可能因為資源耗盡導致 OOM 問題。
首先,我們來看一下 newFixedThreadPool 為什么可能會出現 OOM 的問題。我們寫一段測試代碼,來初始化一個單線程的 FixedThreadPool,循環 1 億次向線程池提交任務,每個任務都會創建一個比較大的字符串然后休眠一小時:
@GetMapping(oom1)public void oom1() throws InterruptedException {ThreadPoolExecutor threadPool = (ThreadPoolExecutor) (1);//打印線程池的信息,稍后我會解釋這段代碼printStats(threadPool); for (int i = 0; i < ; i++) {(() -> {String payload = (1, )(__ -> a)(()) + ()();try {(1);} catch (InterruptedException e) {}(payload);});}();(1, );}執行程序后不久,日志中就出現了如下 OOM:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded翻看 newFixedThreadPool 方法的源碼不難發現,線程池的工作隊列直接 new 了一個 LinkedBlockingQueue,而默認構造方法的 LinkedBlockingQueue 是一個 _VALUE 長度的隊列,可以認為是無界的:
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, ,new LinkedBlockingQueue雖然使用 newFixedThreadPool 可以把工作線程控制在固定的數量上,但任務隊列是無界的。 如果任務較多并且執行較慢的話,隊列可能會快速積壓,撐爆內存導致 OOM。
我們再把剛才的例子稍微改一下,改為使用 newCachedThreadPool 方法來獲得線程池。程序運行不久后,同樣看到了如下 OOM 異常:
[11:30:30.487] [http-nio--exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - () for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is : unable to create new native thread] with root : unable to create new native thread從日志中可以看到,這次 OOM 的原因是無法創建線程,翻看 newCachedThreadPool 的源碼可以看到,這種線程池的最大線程數是 _VALUE,可以認為是沒有上限的,而其工作隊列 SynchronousQueue 是一個沒有存儲空間的阻塞隊列。 這意味著,只要有請求到來,就必須找到一條工作線程來處理,如果當前沒有空閑的線程就再創建一條新的。
由于我們的任務需要 1 小時才能執行完成,大量的任務進來后會創建大量的線程。我們知道線程是需要分配一定的內存空間作為線程棧的,比如 1MB,因此無限制創建線程必然會導致 OOM:
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, _VALUE,60L, ,new SynchronousQueue其實,大部分 Java 開發同學知道這兩種線程池的特性,只是抱有僥幸心理,覺得只是使用線程池做一些輕量級的任務,不可能造成隊列積壓或開啟大量線程。
但,現實往往是殘酷的。 我之前就遇到過這么一個事故:用戶注冊后,我們調用一個外部服務去發送短信,發送短信接口正常時可以在 100 毫秒內響應,TPS 100 的注冊量,CachedThreadPool 能穩定在占用 10 個左右線程的情況下滿足需求。 在某個時間點,外部短信服務不可用了,我們調用這個服務的超時又特別長,比如 1 分鐘,1 分鐘可能就進來了 6000 用戶,產生 6000 個發送短信的任務,需要 6000 個線程,沒多久就因為無法創建線程導致了 OOM,整個應用程序崩潰。
因此,我同樣不建議使用 Executors 提供的兩種快捷的線程池,原因如下:
我們需要根據自己的場景、并發情況來評估線程池的幾個核心參數,包括核心線程數、最大線程數、線程回收策略、工作隊列的類型,以及拒絕策略,確保線程池的工作行為符合需求,一般都需要設置有界的工作隊列和可控的線程數。
任何時候,都應該為自定義線程池指定有意義的名稱,以方便排查問題。 當出現線程數量暴增、線程死鎖、線程占用大量 CPU、線程執行出現異常等問題時,我們往往會抓取線程棧。 此時,有意義的線程名稱,就可以方便我們定位問題。
除了建議手動聲明線程池以外,我還建議用一些監控手段來觀察線程池的狀態。 線程池這個組件往往會表現得任勞任怨、默默無聞,除非是出現了拒絕策略,否則壓力再大都不會拋出一個異常。 如果我們能提前觀察到線程池隊列的積壓,或者線程數量的快速膨脹,往往可以提早發現并解決問題。
二、線程池線程管理策略詳解在之前的 Demo 中,我們用一個 printStats 方法實現了最簡陋的監控,每秒輸出一次線程池的基本內部信息,包括線程數、活躍線程數、完成了多少任務,以及隊列中還有多少積壓任務等信息:
private void printStats(ThreadPoolExecutor threadPool) { ()(() -> {(=========================);(Pool Size: {}, ());(Active Threads: {}, ());(Number of Tasks Completed: {}, ());(Number of Tasks in Queue: {}, ()());(=========================);}, 0, 1, );}接下來,我們就利用這個方法來觀察一下線程池的基本特性吧。
首先,自定義一個線程池。 這個線程池具有 2 個核心線程、5 個最大線程、使用容量為 10 的 ArrayBlockingQueue 阻塞隊列作為工作隊列,使用默認的 AbortPolicy 拒絕策略,也就是任務添加到線程池失敗會拋出 RejectedExecutionException。 此外,我們借助了 Jodd 類庫的 ThreadFactoryBuilder 方法來構造一個線程工廠,實現線程池線程的自定義命名。
@GetMapping(right)public int right() throws InterruptedException {//使用一個計數器跟蹤完成的任務數AtomicInteger atomicInteger = new AtomicInteger();//創建一個具有2個核心線程、5個最大線程,使用容量為10的ArrayBlockingQueue阻塞隊列作為工作隊列的線程池,使用默認的AbortPolicy拒絕策略ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5,5, ,new ArrayBlockingQueue<>(10),new ThreadFactoryBuilder()(demo-threadpool-%d)(),new ());printStats(threadPool);//每隔1秒提交一次,一共提交20次任務(1, 20)(i -> {try {(1);} catch (InterruptedException e) {();}int id = ();try {(() -> {({} started, id);//每個任務耗時10秒try {(10);} catch (InterruptedException e) {}({} finished, id);});} catch (Exception ex) {//提交出現異常的話,打印出錯信息并為計數器減一(error submitting task {}, id, ex);();}});(60);return ();}60 秒后頁面輸出了 17,有 3 次提交失敗了
并且日志中也出現了 3 次類似的錯誤信息:
[14:24:52.879] [http-nio--exec-1] [ERROR] [:103 ] - error submitting task : Task [email protected] rejected from [email protected][Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 2]我們把 printStats 方法打印出的日志繪制成圖表,得出如下曲線:
至此,我們可以總結出線程池默認的工作行為:
不會初始化 corePoolSize 個線程,有任務來了才創建工作線程;
當核心線程滿了之后不會立即擴容線程池,而是把任務堆積到工作隊列中;
當工作隊列滿了后擴容線程池,一直到線程個數達到 maximumPoolSize 為止;
如果隊列已滿且達到了最大線程后還有任務進來,按照拒絕策略處理;
當線程數大于核心線程數時,線程等待 keepAliveTime 后還是沒有任務需要處理的話,收縮線程到核心線程數。
了解這個策略,有助于我們根據實際的容量規劃需求,為線程池設置合適的初始化參數。當然,我們也可以通過一些手段來改變這些默認工作行為,比如:
聲明線程池后立即調用 prestartAllCoreThreads 方法,來啟動所有核心線程;
傳入 true 給 allowCoreThreadTimeOut 方法,來讓線程池在空閑的時候同樣回收核心線程。
不知道你有沒有想過:Java 線程池是先用工作隊列來存放來不及處理的任務,滿了之后再擴容線程池。 當我們的工作隊列設置得很大時,最大線程數這個參數顯得沒有意義,因為隊列很難滿,或者到滿的時候再去擴容線程池已經于事無那么,我們有沒有辦法讓線程池更激進一點,優先開啟更多的線程,而把隊列當成一個后備方案呢?比如我們這個例子,任務執行得很慢,需要 10 秒,如果線程池可以優先擴容到 5 個最大線程,那么這些任務最終都可以完成,而不會因為線程池擴容過晚導致慢任務來不及處理。
這里我只給你一個大致思路:
由于線程池在工作隊列滿了無法入隊的情況下會擴容線程池,那么我們是否可以重寫隊列的 offer 方法,造成這個隊列已滿的假象呢?
由于我們 Hack 了隊列,在達到了最大線程后勢必會觸發拒絕策略,那么能否實現一個自定義的拒絕策略處理程序,這個時候再把任務真正插入隊列呢?
接下來,就請你動手試試看如何實現這樣一個“彈性”線程池吧。 Tomcat 線程池也實現了類似的效果,可供你借鑒。
三、線程池本身是不是復用的不久之前我遇到了這樣一個事故:某項目生產環境時不時有報警提示線程數過多,超過 2000 個,收到報警后查看監控發現,瞬時線程數比較多但過一會兒又會降下來,線程數抖動很厲害,而應用的訪問量變化不大。
為了定位問題,我們在線程數比較高的時候進行線程棧抓取,抓取后發現內存中有 1000 多個自定義線程池。 一般而言,線程池肯定是復用的,有 5 個以內的線程池都可以認為正常,而 1000 多個線程池肯定不正常。
在項目代碼里,我們沒有搜到聲明線程池的地方,搜索 execute 關鍵字后定位到,原來是業務代碼調用了一個類庫來獲得線程池,類似如下的業務代碼:調用 ThreadPoolHelper 的 getThreadPool 方法來獲得線程池,然后提交數個任務到線程池處理,看不出什么異常。
@GetMapping(wrong)public String wrong() throws InterruptedException {ThreadPoolExecutor threadPool = ();(1, 10)(i -> {(() -> { {(1);} catch (InterruptedException e) {}});});return OK;}但是,來到 ThreadPoolHelper 的實現讓人大跌眼鏡,getThreadPool 方法居然是每次都使用 來創建一個線程池。
class ThreadPoolHelper {public static ThreadPoolExecutor getThreadPool() {//線程池沒有復用return (ThreadPoolExecutor) ();}}我們可以想到 newCachedThreadPool 會在需要時創建必要多的線程,業務代碼的一次業務操作會向線程池提交多個慢任務,這樣執行一次業務操作就會開啟多個線程。 如果業務操作并發量較大的話,的確有可能一下子開啟幾千個線程。
那,為什么我們能在監控中看到線程數量會下降,而不會撐爆內存呢?
回到 newCachedThreadPool 的定義就會發現,它的核心線程數是 0,而 keepAliveTime 是 60 秒,也就是在 60 秒之后所有的線程都是可以回收的。 好吧,就因為這個特性,我們的業務程序死得沒太難看。
要修復這個 Bug 也很簡單,使用一個靜態字段來存放線程池的引用,返回線程池的代碼直接返回這個靜態字段即可。 這里一定要記得我們的最佳實踐,手動創建線程池。 修復后的 ThreadPoolHelper 類如下:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded0四、仔細斟酌線程池的混用策略線程池的意義在于復用,那這是不是意味著程序應該始終使用一個線程池呢?要根據任務的“輕重緩急”來指定線程池的核心參數,包括線程數、回收策略和任務隊列:對于執行比較慢、數量不大的 IO 任務,或許要考慮更多的線程數,而不需要太大的隊列。 而對于吞吐量較大的計算型任務,線程數量不宜過多,可以是 CPU 核數或核數2倍(理由是,線程一定調度到某個 CPU 進行執行,如果任務本身是 CPU 綁定的任務,那么過多的線程只會增加線程切換的開銷,并不能提升吞吐量),但可能需要較長的隊列來做緩沖。
業務代碼使用了線程池異步處理一些內存中的數據,但通過監控發現處理得非常慢,整個處理過程都是內存中的計算不涉及 IO 操作,也需要數秒的處理時間,應用程序 CPU 占用也不是特別高,有點不可思議。
經排查發現,業務代碼使用的線程池,還被一個后臺的文件批處理任務用到了?;蛟S是夠用就好的原則,這個線程池只有 2 個核心線程,最大線程也是 2,使用了容量為 100 的 ArrayBlockingQueue 作為工作隊列,使用了 CallerRunsPolicy 拒絕策略:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded1這里模擬一下文件批處理的代碼,在程序啟動后通過一個線程開啟死循環邏輯,不斷向線程池提交任務,任務的邏輯是向一個文件中寫入大量的數據:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded2可以想象到,這個線程池中的 2 個線程任務是相當重的。通過 printStats 方法打印出的日志,我們觀察下線程池的負擔:
可以看到,線程池的 2 個線程始終處于活躍狀態,隊列也基本處于打滿狀態。 因為開啟了 CallerRunsPolicy 拒絕處理策略,所以當線程滿載隊列也滿的情況下,任務會在提交任務的線程,或者說調用 execute 方法的線程執行,也就是說不能認為提交到線程池的任務就一定是異步處理的。 如果使用了 CallerRunsPolicy 策略,那么有可能異步任務變為同步執行。 從日志的第四行也可以看到這點。 這也是這個拒絕策略比較特別的原因。
不知道寫代碼的同學為什么設置這個策略,或許是測試時發現線程池因為任務處理不過來出現了異常,而又不希望線程池丟棄任務,所以最終選擇了這樣的拒絕策略。 不管怎樣,這些日志足以說明線程池是飽和狀態。
可以想象到,業務代碼復用這樣的線程池來做內存計算,命運一定是悲慘的。我們寫一段代碼測試下,向線程池提交一個簡單的任務,這個任務只是休眠 10 毫秒沒有其他邏輯:
Exception in thread http-nio--ClientPoller : GC overhead limit exceeded3我們使用 wrk 工具對這個接口進行一個簡單的壓測,可以看到 TPS 為 75,性能的確非常差。
細想一下,問題其實沒有這么簡單。 因為原來執行 IO 任務的線程池使用的是 CallerRunsPolicy 策略,所以直接使用這個線程池進行異步計算的話,當線程池飽和的時候,計算任務會在執行 Web 請求的 Tomcat 線程執行,這時就會進一步影響到其他同步處理的線程,甚至造成整個應用程序崩潰。
解決方案很簡單,使用獨立的線程池來做這樣的“計算任務”即可。計算任務打
本文詳細介紹了Netty網絡編程框架的核心概念以及入門案例。
1Netty的介紹基于事件驅動的JavaNIO網絡通信框架,可以快速簡單地開發網絡應用程序。
極大地簡化并優化了TCP和UDP套接字服務器等網絡編程,并且性能以及安全性等很多方面甚至都要更好。
支持多種通信協議如FTP,SMTP,HTTP以及各種二進制和基于文本的傳統協議,同樣支持自定義協議。
簡單的說,Netty有三個優點:
高并發:基于NIO開發(Reactor模型),并發性能相比BIO得到了很大提高。
傳輸快:傳輸依賴于零拷貝特性,盡量減少不必要的內存拷貝,使用高性能序列化協議protobuf,實現了高效傳輸。
封裝好:封裝了原始NIO編程的很多細節,提供了易于使用調用接口,使用更簡單。
借用官方的描述:Netty成功地找到了一種在不妥協可維護性和性能的情況下實現易于開發,性能,穩定性和靈活性的方法。
Netty的社區目前非?;钴S。 很多涉及到網絡調用的開源項目和框架底層都用到了Netty,比如我們常用的Dubbo、RocketMQ、Elasticsearch、gRPC、Spark、GateWay等等。
總之,涉及到網絡編程開發時,比如即時通訊系統、自定義RPC框架、自定義HTTP服務器、實時消息推送系統等場景下,用Netty,準沒錯。
2Netty的核心組件2.1Channel通道,Netty網絡操作抽象類,包括基本的I/O操作,如bind、connect、read、write等,Netty的Channel接口所提供的API,大大地降低了直接使用Socket類的復雜性。
不同協議、不同的阻塞類型的連接都有不同的Channel類型與之對應,下面是一些常用的Channel類型:
NioSocketChannel,異步的客戶端TCPSocket連接。
NioServerSocketChannel,異步的服務器端TCPSocket連接。
NioDatagramChannel,異步的UDP連接。
NioSctpChannel,異步的客戶端Sctp連接。
NioSctpServerChannel,異步的Sctp服務器端連接這些通道涵蓋了UDP和TCP網絡IO以及文件IO。
2.2EventLoopEventLoop(事件循環)接口是Netty的核心接口,用于處理連接的生命周期中所發生的各種事件,實際上就是負責監聽網絡事件并調用事件處理器進行相關I/O操作的處理。
EventLoop內部持有NIO中的Selector,Channel將會注冊到EventLoop中,一個EventLoop可以監聽多個Channel,EventLoop是實現IO多路復用的核心,可以看作是Reactor模型中的mainReactor。
Channel為Netty網絡操作抽象類,EventLoop負責監聽注冊到其上的Channel的IO事件,兩者配合完成I/O操作。
2.3ChannelFuture在Netty中所有的IO操作都是異步的,不能立刻得知消息是否被正確處理。
Channel會注冊到EventLoop中后會立即返回一個ChannelFuture對象,可以通過ChannelFuture#addListener注冊GenericFutureListener監聽器,當操作執行成功或失敗時監聽會自動觸發注冊的監聽事件。
2.4ChannelHandlerChannelHandler是消息的具體處理器。 他負責處理各種任務,這個任務非常廣泛,可以是讀寫事件、連接、解碼編碼、數據轉換、業務邏輯等等,處理完畢之后將數據繼續轉發到ChannelPipeline中的下一個ChannelHandler。
通過定制ChannelHandler可對Netty進行擴展。ChannelHandler接口本身并沒有提供很多方法,因為這個接口有許多的方法需要實現,為了方便使用,可以繼承它的子類:
ChannelInboundHandler用于處理入站I/O事件
ChannelOutboundHandler用于處理出站I/O操作
或者使用以下適配器類,更加方便:
ChannelInboundHandlerAdapter用于處理入站I/O事件
ChannelOutboundHandlerAdapter用于處理出站I/O操作
ChannelDuplexHandler用于處理入站和出站事件
2.5ChannelPipelineChannelPipeline是一個ChannelHandler的鏈表,即ChannelHandler組成的List,提供了一個沿著鏈傳播入站和出站事件流的API。
可以在ChannelPipeline上通過addLast()方法添加一個或者多個ChannelHandler,因為一個數據或者事件可能會被多個Handler處理。 當一個ChannelHandler處理完之后就將數據交給下一個ChannelHandler。
在執行時,入站事件會從鏈表head往后傳遞到最后一個入站的handler(ChannelInboundHandler類型),出站事件會從鏈表tail往前傳遞到最前一個出站的handler(ChannelOutboundHandler類型),兩種類型的handler在執行時互不干擾。 如果Handler同時屬于入站、出站Handler,則都會執行一次。
在Netty中每個Channel都有且僅有一個ChannelPipeline與之對應,當Channel被創建時,它會被自動地分配到它專屬的ChannelPipeline。
2.5.1ChannelHandlerContext用于傳輸業務數據,保存Channel相關的所有上下文信息。
將Handler和Pipeline聯系起來,實際上ChannelPipeline中直接存儲的是ChannelHandlerContext,而每個ChannelHandlerContext中又關聯著唯一一個ChannelHandler。
2.5.2入站和出站數據入站,一般是指讀事件觸發,即數據要讀進來;數據從底層的JavaNIOchannel讀取到Netty的Channel,此過程中會進行數據解碼。
數據出站,一般是指寫事件觸發,即數據要寫出去;數據從Netty的Channel寫入底層的JavaNIOchanel,此過程中會進行數據編碼。
入站會從先讀取數據,再執行入站的Handler;出站會先執行出站的Handler,再寫入。
即每次出現讀事件時,會執行入站操作,實際讀取數據之后,會先從頭至尾依次調用ChannelPipeline中的InboundHandler處理,不會調用OutboundHandler;而觸發寫事件時,會執行出站操作,實際寫入數據之前,則會從尾到頭依次調用ChannelPipeline的OutboundHandler處理,不會調用InboundHandler;
下圖描述了ChannelPipeline中的ChannelHandlers通常如何處理I/O事件():
入站事件由入站處理程序按自下而上的方向處理,如圖左側所示。 入站處理程序通常處理由圖底部的I/O線程生成的原始入站數據,例如通過(ByteBuffer)讀取。
出站事件由出站處理程序按自上而下的方向處理,如圖右側所示。 出站處理程序通常會生成或轉換出站流量,例如寫入請求。 如果出站事件超出了底部出站處理程序,則由與通道關聯的I/O線程處理。 I/O線程執行實際的輸出操作,例如通過(ByteBuffer)輸出。
2.6EventLoopGroupEventLoopGroup相當于1個事件循環組,這個組里包含多個事件循環EventLoop,EventLoop的主要作用實際就是負責監聽網絡事件并調用事件處理器進行相關I/O操作的處理。
EventLoopGroup內部的每個EventLoop通常包含1個Selector和1個事件循環線程,一個EventLoop可以綁定多個Channel,但一個Channel只能綁定一個EventLoop,這樣某一個連接的IO事件就在專有的線程上處理,保證線程安全。
NettyServer端包含1個BossNioEventLoopGroup和1個WorkerNioEventLoopGroup:
BossNioEventLoop主要循環執行的工作:
select監聽accept事件。
處理到來的accept事件,與Client建立連接,生成SocketChannel,并將SocketChannel注冊到某個WorkerNioEventLoop的Selector上。
處理任務隊列中的任務,runAllTasks。 任務隊列中的任務包括用戶調用或schedule執行的任務,或者其它線程提交到該eventloop的任務。
WorkerNioEventLoop主要循環執行的工作:
select監聽read、write事件。
處理到來的read、write事件,在NioSocketChannel可讀、可寫事件發生時進行處理。
處理任務隊列中的任務,runAllTasks。
3Netty的線程模型Netty通過Reactor模型基于多路復用器接收并處理用戶請求,內部實現了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept事件,當接收到accept事件的請求時,就會建立連接并把對應的socket封裝到一個NioSocketChannel中,并交給work線程池,其中work線程池負責請求的read和write事件,以及業務邏輯,這些都由對應的Handler處理。
Netty主要靠NioEventLoopGroup線程池的配置來實現具體的線程模型。
3.1單線程模型bossGroup和workerGroup使用同一個NioEventLoopGroup,且配置線程數為1。
適合連接量和并發量都不大的應用。
3.2多線程模型bossGroup和workerGroup使用不同NioEventLoopGroup,且bossGroup配置線程數為1。
適合連接量不大,并發量大的應用。
3.3主從多線程模型bossGroup和workerGroup使用不同NioEventLoopGroup,且都配置為多線程。
適合連接量和并發量都比較大的應用。
從一個主線程NIO線程池中選擇一個線程作為Acceptor線程,綁定監聽端口,接收客戶端連接的連接,其他線程負責后續的接入認證等工作。 連接建立完成后分派給workerGroup線程。
4Netty默認啟動的線程數*EventLoopGroup默認的構造函數實際會起的線程數為CPU核心數2,但bossGroup一般設置數量為1。 EventLoopGroup內部的EventLoop數量就是線程數量,保證1對1的關系。 **
5Netty的啟動過程5.1服務端首先初始化兩個NioEventLoopGroup,其中boosGroup用于處理客戶端建立TCP連接的請求(Accept事件),workerGroup用于處理每一條連接的I/O讀寫事件和具體的業務邏輯。
NioEventLoopGroup類的無參構造函數的默認設置的線程數量是CPU核心數2。 一般情況下我們會指定bossGroup的線程數為1(并發連接量不大的時候),workGroup的線程數量為CPU核心數2。
隨后創建一個ServerBootstrap,它是服務端的啟動引導類/輔助類,它將引導我們進行服務端的啟動工作。 通過ServerBootstrap配置EventLoopGroup、Channel類型,連接參數、配置入站、出站事件handler等。
最后通過bind()方法綁定端口,開始工作。
publicclassNettyServer{staticintport=8888;publicstaticvoidmain(String[]args){//1bossGroup用于接收連接mainReactor//workerGroup用于具體的處理subReactorEventLoopGroupbossGroup=newNioEventLoopGroup(1);EventLoopGroupworkerGroup=newNioEventLoopGroup();try{//2.創建服務端啟動引導/輔助類:ServerBootstrapServerBootstrapserverBootstrap=newServerBootstrap();//3.給引導類配置兩大線程組,確定了線程模型(bossGroup,workerGroup)//4.指定IO模型()(newChannelInitializer首先初始化一個NioEventLoopGroup。
隨后創建一個Bootstrap,它是客戶端的啟動引導類/輔助類,它將引導我們進行客戶端的啟動工作。 通過Bootstrap配置EventLoopGroup、Channel類型,連接參數、配置入站、出站事件handler等。
最后通過connect()方法使用服務端的ip和port進行連接,開始工作。
publicclassNettyClient{staticintport=8888;staticStringhost=127.0.0.1;publicstaticvoidmain(String[]args){//1.創建一個NioEventLoopGroup對象實例EventLoopGroupgroup=newNioEventLoopGroup();try{//2.創建客戶端啟動引導/輔助類:BootstrapBootstrapbootstrap=newBootstrap();//3.指定線程組(group)//4.指定IO模型()(newChannelInitializerTCP是以流的方式來處理數據,底層會有一個緩沖區,一個完整的較大的包可能會被TCP拆分成多個包進行發送,也可能把多個小的包封裝成一個大的數據包發送。
TCP粘包/拆包的原因:應用程序寫入的字節大小大于套接字發送緩沖區的大小,會發生拆包現象,實際表現就是不能收到完整的消息。 而應用程序寫入數據小于套接字緩沖區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘包現象,實際表現就是一次性收到多條粘連在一起消息。
報頭的選項字段有MSS(MaximumSegmentSize,最大報文段大?。┳侄?,規定一個TCP包最大可傳輸的字節數,一般是1500-20-20=1460字節,大于該大小時將發生拆包。
6.2解決辦法使用Netty自帶的解碼器
LineBasedFrameDecoder:發送端發送數據包的時候,每個數據包之間以換行符作為分隔,即\n或者\r\n,其工作原理是它依次遍歷ByteBuf中的可讀字節,判斷是否有換行符,然后進行相應的截取。
DelimiterBasedFrameDecoder:可以自定義分隔符解碼器,其實際上是一種特殊的DelimiterBasedFrameDecoder解碼器。
FixedLengthFrameDecoder:固定長度解碼器,它能夠按照指定的長度對消息進行相應的拆包。 需要約定每一個包的固定大小。
LengthFieldBasedFrameDecoder:將消息分為消息頭和消息體。 在頭部中保存有當前整個消息的長度,只有在讀取到足夠長度的消息之后才算是讀到了一個完整的消息。
通過自定義協議進行粘包和拆包的處理。
7Netty的長連接、心跳機制Netty客戶端和服務器采用長連接保持聯系。 client與server完成一次讀寫,它們之間的連接并不會主動關閉,后續的讀寫操作會繼續使用這個連接。 長連接的可以省去較多的TCP建立和關閉的操作,降低對網絡資源的依賴,節約時間。
在TCP保持長連接的過程中,可能會出現斷網等網絡異常出現,異常發生的時候,client與server之間如果沒有交互的話,它們是無法發現對方已經掉線的。 為了解決這個問題,我們就需要引入心跳機制。
心跳機制的工作原理是:在client與server之間在一定時間內沒有數據交互時,即處于idle狀態時,客戶端或服務器就會發送一個特殊的數據包給對方,當接收方收到這個數據報文后,也立即發送一個特殊的數據報文,回應發送方,此即一個PING-PONG交互。 所以,當某一端收到心跳消息后,就知道了對方仍然在線,這就確保TCP連接的有效性。
TCP實際上自帶的就有長連接選項,本身是也有心跳包機制,也就是TCP的選項:SO_KEEPALIVE。 但是,TCP協議層面的長連接靈活性不夠。 所以,一般情況下我們都是在應用層協議上實現自定義心跳機制的,也就是在Netty層面通過編碼實現。 通過Netty實現心跳機制的話,核心類是IdleStateHandler。
Netty支持的哪些心跳類型設置:
readerIdleTime:為讀超時時間(即測試端一定時間內未接受到被測試端消息)。
writerIdleTime:為寫超時時間(即測試端一定時間內向被測試端發送消息)。
allIdleTime:所有類型的超時時間。
8Netty的零拷貝零復制(英語:Zero-copy;也譯零拷貝)技術是指計算機執行操作時,CPU不需要先將數據從某處內存復制到另一個特定區域。 這種技術通常用于通過網絡傳輸文件時節省CPU周期和內存帶寬。
Netty中的零拷貝體現在以下幾個方面:
Netty提供了CompositeByteBuf類,可以將多個ByteBuf合并為一個邏輯上的ByteBuf,避免了各個ByteBuf之間的數據拷貝。
ByteBuf支持slice操作,因此可以將ByteBuf分解為多個共享同一個存儲區域的ByteBuf,避免了內存的拷貝。
通過FileRegion包裝的實現文件傳輸,可以直接將文件緩沖區的數據發送到目標Channel,避免了傳統通過循環write方式導致的內存拷貝問題。
Netty的接收和發送ByteBuffer采用DIRECTBUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩沖區的二次拷貝。 如果使用傳統的堆內存(HEAPBUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,然后才寫入Socket中。 相比于堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。
9Netty和Tomcat的區別作用不同:Tomcat是Servlet容器,可以視為Web服務器,是一款已經開發好的軟件,而Netty是一款強大的異步事件驅動的網絡應用程序框架,用于簡化網絡編程,可用于編寫各種服務器。
協議不同:Tomcat是基于http協議的Web服務器,而Netty支持各種現成的協議并且能通過編程自定義各種協議,因為Netty本身自己能編碼/解碼字節流,所以Netty可以實現HTTP服務器、FTP服務器、UDP服務器、RPC服務器、WebSocket服務器、Redis的Proxy服務器、MySQL的Proxy服務器等等。
10Netty簡單案例 publicclassNettyClient{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{//1.創建一個NioEventLoopGroup對象實例EventLoopGroupgroup=newNioEventLoopGroup();try{//2.創建客戶端啟動引導/輔助類:BootstrapBootstrapbootstrap=newBo在Web2.0的浪潮中,各種頁面技術和框架不斷涌現,為服務器端的基礎架構提出了更高的穩定性和可擴展性的要求。 近年來,作為開源中間件的全球領導者,JBoss在J2EE應用服務器領域已成為發展最為迅速的應用服務器。 在市場占有率和服務滿意度上取得了巨大的成功,絲毫不遜色于其它的非開源競爭對手,如WebSphere、WebLogic、Application Server。 JBoss Web的諸多優越性能,正是其廣為流行的原因。 基于Tomcat內核,青勝于藍Tomcat 服務器是一個免費的開放源代碼的Web 應用服務器,技術先進、性能穩定,而且免費,因而深受Java 愛好者的喜愛并得到了部分軟件開發商的認可。 其運行時占用的系統資源小,擴展性好,且支持負載平衡與郵件服務等開發應用系統常用的功能。 作為一個小型的輕量級應用服務器,Tomcat在中小型系統和并發訪問用戶不是很多的場合下被普遍使用,成為目前比較流行的Web 應用服務器。 而JBoss Web采用業界最優的開源Java Web引擎, 將Java社區中下載量最大,用戶數最多,標準支持最完備的Tomcat內核作為其Servlet容器引擎,并加以審核和調優。 單純的Tomcat性能有限,在很多地方表現有欠缺,如活動連接支持、靜態內容、大文件和HTTPS等。 除了性能問題,Tomcat的另一大缺點是它是一個受限的集成平臺,僅能運行Java應用程序。 企業在使用時Tomcat,往往還需同時部署Apache Web Server以與之整合。 此配置較為繁瑣,且不能保證性能的優越性。 JBoss在Tomcat的基礎上,對其進行本地化,將Tomcat 以內嵌的方式集成到 JBoss 中。 JBoss Web通過使用APR和Tomcat本地技術的混合模型來解決Tomcat的諸多不足。 混合技術模型從最新的操作系統技術里提供了最好的線程和事件處理。 結果,JBoss Web達到了可擴展性,性能參數匹配甚至超越了本地Apache HTTP服務器或者IIS。 譬如JBoss Web能夠提供數據庫連接池服務,不僅支持 JSP 等 Java 技術,同時還支持其他 Web 技術的集成,譬如 PHP、 兩大陣營。 標準化是減小技術依賴風險,保護投資最好的方式。 JBoss Web率先支持全系列JEE Web標準,從根本上保證了應用“一次開發,到處運行”的特點,使應用成品能方便地在JBoss Web和其他Java Web服務器之間輕易遷移。 集多功能于一身,性能卓越作為Web 應用服務器中的明星產品,JBoss Web服務器集多種功能于一身。 其關鍵功能包括:完全支持Java EE、高度的擴展性、快速的靜態內容處理、群集、OpenSSL、URL重寫和綜合性。 JBoss Web服務器具有原生特性和強大的可擴展性,可支持多種并非基于Java的服務器內容處理技術,可同時運行JSP, Servlet, Microsoft , PHP 及 CGI,為其提供一個單一的、高性能的企業級部署平臺。 與Tomcat 相比,JBoss Web在靜態資源訪問方面性能優越。 JBoss Web支持兩種組件模式——純Java和Native I/O。 在Native組件的支持下,動態運行不會受到任何影響,而靜態資源的訪問利用了操作系統本身提供的0拷貝傳送,CPU消耗降低,響應時間縮短,吞吐率大大提高,混合的連接模式支持最大達到個并發客戶端的同時訪問,與Apache Web服務器相當。 部署于高性能的操作系統,可利用JBoss Web對純Java和Native I/O兩種模式的支持,使得應用在開發時可隨時跨平臺敏捷遷移,而部署于高性能的操作系統相關的Native環境。 由于JBoss Web較好地解決了靜態資源的訪問性能問題,可在解決方案中把它直接作為強大的LVS的分發對象,和RHEL負載均衡系統結合,形成理論上無限線性擴展的負載均衡場景。 OpenSSL是業界最為快速和安全的開源傳輸組件,可借助操作系統和硬件的特性實現高效的安全承載。 JBoss Web集成了OpenSSL,可提供高效的安全傳輸服務,使得安全機制更上臺階。 研究表明, JBoss Web中的SSL性能比單純的Tomcat快四倍。 URL重寫功能可縮短URL,隱藏實際路徑提高安全性,易于用戶記憶和鍵入,及被搜索引擎收錄。 Tomcat 不具備URL重寫功能,JBoss Web則可提供一個靈活的URL rewriting操作引擎,支持無限個規則數和規則條件。 URL可被重寫以支持遺留的URL錯誤處理,或應對服務器不時產生的其他問題。 JBoss Web既可單獨運行,也可無縫嵌入JBoss應用服務器,成為JBoss中間件平臺的一部分。 不僅后臺服務調用的性能將得以提升,也可利用以下JBoss平臺的特性提升Web應用功能:基于JGroups的多種集群方案的支持基于Arjuna技術的JTA和JTS的事務處理支持優化的線程池和連接池的支持基于JMX 控制臺的基本管理支持和JBoss On的高級管理維護支持基于JBoss AOP技術的面向方面架構的支持Hibernate服務組件的支持專業團隊支持業界大多數開源產品在技術方面富于創新性,但在可持續性,產品生命周期規劃,以及質量保證方面缺乏有效保障,為軟件集成商和最終用戶所詬病。 紅帽所力行的“專業化開源技術”則完美解決了這一問題。 來自開源社區的JBoss Web,在紅帽專業化開源的錘煉下,在性能、擴展性、穩定性、安全性等方面,已成為一個達到企業級,甚至電信級標準的優秀產品。 紅帽不僅有專職的技術團隊投入JBoss Web的開發,而且具備專門的QA團隊為產品作質量保證。 完善的集成測試和兼容性測試保證了JBoss Web自身的穩定性,并保證了它的后向兼容和其他JBoss產品協作良好的互操作性。 在服務體系保障方面,JBoss 開拓了以產品專家提供的專家級支持服務作為開源軟件強大后盾的軟件生態模式。 公司以及龐大的 JBoss 授權服務合作伙伴網絡可為包括JBoss Web在內的整個JEMS 產品套件提供全面的支持服務。 與Tomcat相比,JBoss Web 可提供遷移服務與現場專家服務,在遷移服務方面,專家指導應用可從Tomcat向JBoss Web遷移,省時省力。 獨特的服務訂閱模式,全力保障軟件生命周期,讓企業高枕無憂。
通常提到線程安全問題等就有可能聽到關線程安全和并發工具的一些片面的觀點和結論。 比如“把 HashMap 改為 ConcurrentHashMap,就可以解決并發問題了呀”“要不我們試試無鎖的 CopyOnWriteArrayList 吧,性能更好”。 的確,為了方便開發者進行多線程編程,現代編程語言會提供各種并發工具類。 但如果我們沒有充分了解它們的使用場景、解決的問題,以及最佳實踐的話,盲目使用就可能會導致一些坑,小則損失性能,大則無法確保多線程情況下業務邏輯正確性。
一、線程重用導致用戶信息錯亂的Bug之前有業務同學和我反饋,在生產上遇到一個詭異的問題,有時獲取到的用戶信息是別人的。 查看代碼后,我發現他使用了ThreadLocal 來緩存獲取到的用戶信息。 我們知道,ThreadLocal 適用于變量在線程間隔離,而在方法或類間共享的場景。 如果用戶信息的獲取比較昂貴(比如從數據庫查詢用戶信息),那么在 ThreadLocal 中緩存數據是比較合適的做法。 但,這么做為什么會出現用戶信息錯亂的 Bug 呢?
使用 Spring Boot 創建一個 Web 應用程序,使用 ThreadLocal 存放一個 Integer 的值,來暫且代表需要在線程中保存的用戶信息,這個值初始是 null。 在業務邏輯中,我先從 ThreadLocal 獲取一次值,然后把外部傳入的參數設置到 ThreadLocal 中,來模擬從當前上下文獲取到用戶信息的邏輯,隨后再獲取一次值,最后輸出兩次獲得的值和線程名稱。
private static final ThreadLocal按理說,在設置用戶信息之前第一次獲取的值始終應該是 null,但我們要意識到,程序運行在 Tomcat 中,執行程序的線程是 Tomcat 的工作線程,而 Tomcat 的工作線程是基于線程池的。 顧名思義,線程池會重用固定的幾個線程,一旦線程重用,那么很可能首次從 ThreadLocal 獲取的值是之前其他用戶的請求遺留的值。 這時,ThreadLocal 中的用戶信息就是其他用戶的信息。
為了更快地重現這個問題,我在配置文件中設置一下 Tomcat 的參數,把工作線程池最大線程數設置為 1,這樣始終是同一個線程在處理請求:
隨后用戶 2 來請求接口,這次就出現了 Bug,第一和第二次獲取到用戶 ID 分別是 1 和 2,如果是按照正常的來說數的應該是的null和2。 顯然第一次獲取到了用戶 1 的信息,原因就是Tomcat 的線程池重用了線程。 從圖中可以看到,兩次請求的線程都是同一個線程:http-nio-8080-exec-1。
這個例子告訴我們,在寫業務代碼時,首先要理解代碼會跑在什么線程上:
我們可能會抱怨學多線程沒用,因為代碼里沒有開啟使用多線程。 但其實,可能只是我們沒有意識到,在 Tomcat 這種 Web 服務器下跑的業務代碼,本來就運行在一個多線程環境(否則接口也不可能支持這么高的并發),并不能認為沒有顯式開啟多線程就不會有線程安全問題。
因為線程的創建比較昂貴,所以 Web 服務器往往會使用線程池來處理請求,這就意味著線程會被重用。 這時,使用類似 ThreadLocal 工具來存放一些數據時,需要特別注意在代碼運行完后,顯式地去清空設置的數據。 如果在代碼中使用了自定義的線程池,也同樣會遇到這個問題。
理解了這個知識點后,我們修正這段代碼的方案是,在代碼的 finally 代碼塊中,顯式清除 ThreadLocal 中的數據。 這樣一來,新的請求過來即使使用了之前的線程也不會獲取到錯誤的用戶信息了。 修正后的代碼如下:
@GetMapping(right)public Map right(@RequestParam(userId) Integer userId) {String before= ()() + : + ();(userId);try {String after = ()() + : + ();Map result = new HashMap();(before, before);(after, after);return result;} finally {//在finally代碼塊中刪除ThreadLocal中的數據,確保數據不串();}}重新運行程序可以驗證,再也不會出現第一次查詢用戶信息查詢到之前用戶請求的 Bug:
ThreadLocal 是利用獨占資源的方式,來解決線程安全問題,那如果我們確實需要有資源在線程之間共享,應該怎么辦呢?這時,我們可能就需要用到線程安全的容器了。
二、并發工具導致的線程安全問題JDK 1.5 后推出的 ConcurrentHashMap,是一個高性能的線程安全的哈希表容器。 “線程安全”這四個字特別容易讓人誤解,因為 ConcurrentHashMap 只能保證提供的原子性讀寫操作是線程安全的。 我在相當多的業務代碼中看到過這個誤區,比如下面這個場景。 有一個含 900 個元素的 Map,現在再補充 100 個元素進去,這個補充操作由 10 個線程并發進行。 開發人員誤以為使用了 ConcurrentHashMap 就不會有線程安全問題,于是不加思索地寫出了下面的代碼:在每一個線程的代碼邏輯中先通過 size 方法拿到當前元素數量,計算 ConcurrentHashMap 目前還需要補充多少元素,并在日志中輸出了這個值,然后通過 putAll 方法把缺少的元素添加進去。
//線程個數private static int THREAD_COUNT = 10;//總元素數量private static int ITEM_COUNT = 1000;//幫助方法,用來獲得一個指定元素數量模擬數據的ConcurrentHashMapprivate ConcurrentHashMap從日志中可以看到:
初始大小 900 符合預期,還需要填充 100 個元素。
worker1 線程查詢到當前需要填充的元素為 36,竟然還不是 100 的倍數。 worker13 線程查詢到需要填充的元素數是負的,顯然已經過度填充了。 最后 HashMap 的總項目數是 1536,顯然不符合填充滿 1000 的預期。
針對這個場景,我們可以舉一個形象的例子。 ConcurrentHashMap 就像是一個大籃子,現在這個籃子里有 900 個桔子,我們期望把這個籃子裝滿 1000 個桔子,也就是再裝 100 個桔子。 有 10 個工人來干這件事兒,大家先后到崗后會計算還需要補多少個桔子進去,最后把桔子裝入籃子。
ConcurrentHashMap 這個籃子本身,可以確保多個工人在裝東西進去時,不會相互影響干擾,但無法確保工人 A 看到還需要裝 100 個桔子但是還未裝的時候,工人 B 就看不到籃子中的桔子數量。 更值得注意的是,你往這個籃子裝 100 個桔子的操作不是原子性的,在別人看來可能會有一個瞬間籃子里有 964 個桔子,還需要補 36 個桔子。
回到 ConcurrentHashMap,我們需要注意 ConcurrentHashMap 對外提供的方法或能力的限制:
使用了 ConcurrentHashMap,不代表對它的多個操作之間的狀態是一致的,是沒有其他線程在操作它的,如果需要確保需要手動加鎖。
諸如 size、isEmpty 和 containsValue 等聚合方法,在并發情況下可能會反映 ConcurrentHashMap 的中間狀態。 因此在并發情況下,這些方法的返回值只能用作參考,而不能用于流程控制。 顯然,利用 size 方法計算差異值,是一個流程控制。 諸如 putAll 這樣的聚合方法也不能確保原子性,在 putAll 的過程中去獲取數據可能會獲取到部分數據。
@GetMapping(right)public String right() throws InterruptedException {ConcurrentHashMap可以看到,只有一個線程查詢到了需要補 100 個元素,其他 9 個線程查詢到不需要補元素,最后 Map 大小為 1000。 到了這里,你可能又要問了,使用 ConcurrentHashMap 全程加鎖,還不如使用普通的 HashMap 呢。
其實不完全是這樣。 ConcurrentHashMap 提供了一些原子性的簡單復合邏輯方法,用好這些方法就可以發揮其威力。 這就引申出代碼中常見的另一個問題:在使用一些類庫提供的高級工具類時,開發人員可能還是按照舊的方式去使用這些新類,因為沒有使用其特性,所以無法發揮其威力。
三、并發工具的特性,導致性能降低問題我們來看一個使用 Map 來統計 Key 出現次數的場景吧,這個邏輯在業務代碼中非常常見。
使用 ConcurrentHashMap 來統計,Key 的范圍是 10。
使用最多 10 個并發,循環操作 1000 萬次,每次操作累加隨機的 Key。
如果 Key 不存在的話,首次設置值為 1。
//循環次數private static int LOOP_COUNT = ;//線程數量private static int THREAD_COUNT = 10;//元素數量private static int ITEM_COUNT = 10;private Map我們吸取之前的教訓,直接通過鎖的方式鎖住 Map,然后做判斷、讀取現在的累計值、加 1、保存累加后值的邏輯。這段代碼在功能上沒有問題,但無法充分發揮 ConcurrentHashMap 的威力,改進后的代碼如下:
private Map使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 來做復合邏輯操作,判斷 Key 是否存在 Value,如果不存在則把 Lambda 表達式運行后的結果放入 Map 作為 Value,也就是新創建一個 LongAdder 對象,最后返回 Value。
由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一個線程安全的累加器,因此可以直接調用其 increment 方法進行累加。
這樣在確保線程安全的情況下達到極致性能,把之前 7 行代碼替換為了 1 行。
@GetMapping(good)public String good() throws InterruptedException {StopWatch stopWatch = new StopWatch();(normaluse);Map這段測試代碼并無特殊之處,使用 StopWatch 來測試兩段代碼的性能,最后跟了一個斷言判斷 Map 中元素的個數以及所有 Value 的和,是否符合預期來校驗代碼的正確性。測試結果如下:
可以看到,優化后的代碼,相比使用鎖來操作 ConcurrentHashMap 的方式,性能提升了 10 倍。 你可能會問,computeIfAbsent 為什么如此高效呢?答案就在源碼最核心的部分,也就是 Java 自帶的 Unsafe 實現的 CAS。 它在虛擬機層面確保了寫入數據的原子性,比加鎖的效率高得多:
static final像 ConcurrentHashMap 這樣的高級并發工具的確提供了一些高級 API,只有充分了解其特性才能最大化其威力,而不能因為其足夠高級、酷炫盲目使用。
四、不熟悉并發工具的使用場景,因而導致性能問題除了 ConcurrentHashMap 這樣通用的并發工具類之外,我們的工具包中還有些針對特殊場景實現的生面孔。 一般來說,針對通用場景的通用解決方案,在所有場景下性能都還可以,屬于“萬金油”;而針對特殊場景的特殊實現,會有比通用解決方案更高的性能,但一定要在它針對的場景下使用,否則可能會產生性能問題甚至是 Bug。
之前在排查一個生產性能問題時,我們發現一段簡單的非數據庫操作的業務邏輯,消耗了超出預期的時間,在修改數據時操作本地緩存比回寫數據庫慢許多。 查看代碼發現,開發同學使用了 CopyOnWriteArrayList 來緩存大量的數據,而數據變化又比較頻繁。
CopyOnWrite 是一個時髦的技術,不管是 Linux 還是 Redis 都會用到。 在 Java 中,CopyOnWriteArrayList 雖然是一個線程安全的 ArrayList,但因為其實現方式是,每次修改數據時都會復制一份數據出來,所以有明顯的適用場景,即讀多寫少或者說希望無鎖讀的場景。
如果我們要使用 CopyOnWriteArrayList,那一定是因為場景需要而不是因為足夠酷炫。 如果讀寫比例均衡或者有大量寫操作的話,使用 CopyOnWriteArrayList 的性能會非常糟糕。
我們寫一段測試代碼,來比較下使用 CopyOnWriteArrayList 和
Tomcat 或者 Jetty 就是一個“HTTP 服務器 + Servlet 容器”,我們也叫它們 Web 容器。
Spring 框架就是對 Servlet 的封裝,Spring 應用本身就是一個 Servlet,而 Servlet 容器是管理和運行 Servlet 的。
Servlet 接口和 Servlet 容器這一整套規范叫作 Servlet 規范。 Tomcat 和 Jetty 都按照 Servlet 規范的要求實現了 Servlet 容器。
Servlet 容器工作流程:
當客戶請求某個資源時,HTTP 服務器會用一個 ServletRequest 對象把客戶的請求信息封裝起來,然后調用 Servlet 容器的 service 方法,Servlet 容器拿到請求后,根據請求的 URL 和 Servlet 的映射關系,找到相應的 Servlet,如果 Servlet 還沒有被加載,就用反射機制創建這個 Servlet,并調用 Servlet 的 init 方法來完成初始化,接著調用 Servlet 的 service 方法來處理請求,把 ServletResponse 對象返回給 HTTP 服務器,HTTP 服務器會把響應發送給客戶端。
Servlet 規范提供了兩種擴展機制:Filter和Listener。
Tomcat 要實現 2 個核心功能:
因此 Tomcat 設計了兩個核心組件連接器(Connector)和容器(Container)來分別做這兩件事情。 連接器負責對外交流,容器負責內部處理。
1,連接器
連接器需要完成 3 個高內聚的功能:
因此 Tomcat 的設計者設計了 3 個組件來實現這 3 個功能,分別是 EndPoint、Processor 和 Adapter。
Endpoint 和 Processor 放在一起抽象成了 ProtocolHandler 組件,連接器用 ProtocolHandler 來處理網絡連接和應用層協議。
EndPoint 是一個接口,它的抽象實現類 AbstractEndpoint 里面定義了兩個內部類:Acceptor 和 SocketProcessor。 其中 Acceptor 用于監聽 Socket 連接請求。 SocketProcessor 用于處理接收到的 Socket 請求。
EndPoint 接收到 Socket 連接后,生成一個 SocketProcessor 任務提交到線程池去處理,SocketProcessor 的 Run 方法會調用 Processor 組件去解析應用層協議,Processor 通過解析生成 Request 對象后,會調用 Adapter 的 Service 方法。
2,容器
Tomcat 設計了 4 種容器,分別是 Engine、Host、Context 和 Wrapper。 這 4 種容器不是平行關系,而是父子關系。
Context 表示一個 Web 應用程序;Wrapper 表示一個 Servlet,一個 Web 應用程序中可能會有多個 Servlet;Host 代表的是一個虛擬主機,或者說一個站點,可以給 Tomcat 配置多個虛擬主機地址,而一個虛擬主機下可以部署多個 Web 應用程序;Engine 表示引擎,用來管理多個虛擬站點,一個 Service 最多只能有一個 Engine。
請求定位 Servlet 的過程:Tomcat 會創建一個 Service 組件和一個 Engine 容器組件,在 Engine 容器下創建兩個 Host 子容器,在每個 Host 容器下創建兩個 Context 子容器。 由于一個 Web 應用通常有多個 Servlet,Tomcat 還會在每個 Context 容器里創建多個 Wrapper 子容器。
每一個容器都有一個 Pipeline 對象。
3,一個請求在 Tomcat 中流轉的過程 :
4, 啟動 tomcat 的過程:
粉絲福利,需獲取Tomcat、spring等架構資料
內容聲明:
1、本站收錄的內容來源于大數據收集,版權歸原網站所有!
2、本站收錄的內容若侵害到您的利益,請聯系我們進行刪除處理!
3、本站不接受違法信息,如您發現違法內容,請聯系我們進行舉報處理!
4、本文地址:http://m.sycxjdsbhs.com/article/6334d38ab177f8c5eeb4.html,復制請保留版權鏈接!
body,font,family,Arial,Helvetica,sans,serif,h1,margin,bottom,15px,.container,width,80%,margin,0auto,.section,margin,bottom,30px,.sub,section,margin,bottom,15px,.co...。
技術教程 2024-09-30 01:53:38
在當今數據驅動的世界中,并行編程已成為應對海量計算需求至關重要的工具,OpenCL,開放式計算語言,作為一種異構并行編程語言和框架,為開發者提供了利用各種計算設備,如CPU、GPU和加速器,的強大能力,OpenCL概述OpenCL是一種跨平臺語言,允許開發者使用單一編程模型針對不同的異構設備編寫并行應用程序,它采用C99語言規范,并提...。
互聯網資訊 2024-09-28 16:21:08
AWK是一種強大的文本處理語言,通過掌握其高級特性,可以進一步提高處理效率和擴展AWK的功能,內置函數length,string,返回字符串的長度,substr,string,start,length,從字符串中提取子字符串,split,string,array,sep,根據分隔符將字符串拆分為數組,index,string,s...。
本站公告 2024-09-25 18:12:10
簡介fscanf是C標準庫中用于從文本文件中讀取格式化數據的靈活函數,它允許您指定一個格式字符串,其中包含各種格式說明符,如%d,整數,、%f,浮點數,和%s,字符串,fscanf將根據格式字符串,從文件中讀取與之匹配的數據,在本文中,我們將分步學習如何使用fscanf從文本文件中讀取學生信息,包括姓名、學號、分數等,步驟1,打開文...。
本站公告 2024-09-23 17:15:56
03cli>,Facebook,社交媒體巨頭使用ReactNative構建了其移動應用程序,Instagram,照片分享應用程序使用ReactNative重新設計了其界面,Airbnb,住宿預訂應用程序使用ReactNative提高了其性能和用戶體驗,掌握人生掌握ReactNative的高級特性可以幫助開發人員構建功能強大的移動應...。
最新資訊 2024-09-14 11:51:57
引言設計模式是解決常見軟件設計問題的通用解決方案,它們通過提供經過驗證的、可重用的設計原則和元素,幫助開發者編寫可維護、可擴展和靈活的代碼,本文將探討PHP中常見的設計模式,包括它們的用途、優點和實現示例,了解這些模式將使你能夠構建強大、可擴展的PHP應用程序,常見的設計模式1.工廠模式用途,創建一個對象,而無需指定其確切類型,優點,...。
本站公告 2024-09-12 22:05:05
在SEO和內容營銷中,標題扮演著至關重要的角色,一個有吸引力的標題可以吸引讀者,讓他們點擊你的內容,而一個平淡的標題則會讓他們錯過你的精彩文章,標簽是提升標題吸引力的一種有效方式,通過使用相關的標簽,你可以向讀者展示你的內容是如何與他們的興趣和搜索查詢相關的,5個提升標題吸引力的標簽技巧1.使用相關的、具體標簽不要使用籠統、通用的標簽...。
最新資訊 2024-09-12 07:14:59
在現代軟件開發中,數據庫測試自動化已成為不可或缺的一部分,它可以顯著提高測試效率、準確性和代碼質量,本文將深入探討數據庫測試自動化的重要性、優勢和最佳實踐,幫助您充分發揮其潛力,為什么要進行數據庫測試自動化,數據庫是許多軟件應用程序的關鍵組件,存儲著應用程序的關鍵數據,確保數據庫的行為符合預期對于應用程序的穩定性和可靠性至關重要,手動...。
技術教程 2024-09-09 12:22:52
引言在現代軟件開發中,依賴注入是一種常用的設計模式,它可以提高應用程序的模塊化、可測試性和靈活性,Java接口在依賴注入中的應用尤為廣泛,因為它提供了以下優勢,簡潔,接口只聲明方法簽名,不包含任何實現細節,使得代碼更加簡潔易讀,可測試,接口可以被模擬或存根,這使得測試應用程序變得更加容易,靈活,接口允許開發者在運行時動態注入不同的實現...。
互聯網資訊 2024-09-08 12:40:51
在當今以技術為中心的商業環境中,電子商務已成為必不可少的驅動力,為企業提供了擴大其市場覆蓋范圍,增加收入并建立忠實客戶群體的巨大機會,為了充分利用電子商務的潛力,選擇一個可靠且功能強大的源碼解決方案至關重要,開源解決方案提供了一系列好處,例如靈活性、可定制性以及更低的運營成本,使其成為電子商務企業的理想選擇,在本文中,我們將探索領先的...。
最新資訊 2024-09-07 07:24:20
前言遞歸函數是一種在問題求解中發揮著至關重要作用的強大工具,它們通過以較小規模的方式重復調用自身來解決復雜問題,在MATLAB中,遞歸函數的使用為解決各種計算問題提供了靈活且高效的途徑,遞歸函數的本質遞歸函數遵循兩個關鍵原則,1.基本案例,函數定義有一個或多個基本案例,這些案例指定問題如何針對最簡單的情況進行求解,2.遞歸步驟,對于基...。
互聯網資訊 2024-09-07 05:35:19
在當今競爭激烈的廣告環境中,脫穎而出并吸引受眾的注意力至關重要,對聯廣告策略是一種有效的技術,它利用創意和吸引力,幫助廣告客戶創建引人注目的、難忘的廣告,對聯廣告策略簡介對聯廣告策略是將兩個或更多相關或互補的廣告配對的做法,這些廣告通常并排或上下放置,在視覺上相互補充,并傳達一個連貫的信息,對聯廣告策略的優勢提高可見度,對聯廣告通過在...。
互聯網資訊 2024-09-06 19:45:23