前言 從0到1構建分散式秒殺系統案例的代碼已經全部上傳至碼雲,文章也被分發到各個平臺。其中也收到了不少小伙伴喜歡和反饋,有網友如是說: 說實話,能用上的不多,中小企業都不可能用到,大型企業也不是一個人就能搞起的,大部分人一輩子都用不上,等有這個需要再搞吧。 我的觀點是贊同但不支持,基本上任何事物都是 ...
前言
從0到1構建分散式秒殺系統案例的代碼已經全部上傳至碼雲,文章也被分發到各個平臺。其中也收到了不少小伙伴喜歡和反饋,有網友如是說:
說實話,能用上的不多,中小企業都不可能用到,大型企業也不是一個人就能搞起的,大部分人一輩子都用不上,等有這個需要再搞吧。
我的觀點是贊同但不支持,基本上任何事物都是呈金字塔分佈,互聯網也不例外,也就是說大部分可能都是普通人,接觸不到所謂大廠的應用場景。但是,書到用時方恨少,機會總是留給有準備的人的,除非有錢難買我樂意,只能說大千世界,每個人都有自己的生活方式,尊重並活著。
進程和線程
前面都是扯淡,也不是什麼鋪墊,在聊線程池之前我們最好簡單瞭解下什麼是進程,什麼是線程,進程和線程到底有什麼區別?
這裡我們,搬運下某百科的釋義:
進程是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的電腦結構中,進程是程式的基本執行實體;在當代面向線程設計的電腦結構中,進程是線程的容器。程式是指令、數據及其組織形式的描述,進程是程式的實體。
當然,知乎上也有不少網友的回答,每個人都有自己不同的理解方式。這裡我們拿Tomcat容器做例子:你可以這麼理解,運行中的Tomcat容器就是一個進程,而每個用戶的操作(查詢、上傳)可以當做一個或者多個線程。
線程池
秒殺活動中,瞬時併發是非常大的,如果每一個請求都開啟一個新線程,系統就要不斷的進行線程的創建和銷毀,有時花在創建和銷毀線程上的時間會比線程真正執行的時間還長。並且由於硬體條件限制,線程數量又不能無限創建。
那麼線程池到底解決了那些問題:
- 降低資源消耗:通過重用已經創建的線程來降低線程創建和銷毀的消耗
- 提高響應速度:任務到達時不需要等待線程創建就可以立即執行
- 提高線程的可管理性:線程池可以統一管理、分配、調優和監控
執行流程
調用ThreadPoolExecutor的execute提交線程,首先檢查CorePool,如果CorePool內的線程小於CorePoolSize,新創建線程執行任務。
如果當前CorePool內的線程大於等於CorePoolSize,那麼將線程加入到BlockingQueue。
如果不能加入BlockingQueue,在小於MaxPoolSize的情況下創建線程執行任務。
如果線程數大於等於MaxPoolSize,那麼執行拒絕策略。
模擬測試
為了方便測試,我們在Control中定義了線程池,來模擬用戶秒殺動作:
定義初始線程數:
private static int corePoolSize = Runtime.getRuntime().availableProcessors();
- IO密集型任務 = 一般為2*CPU核心數(常出現於線程中:資料庫數據交互、文件上傳下載、網路數據傳輸等等)
- CPU密集型任務 = 一般為CPU核心數+1(常出現於線程中:複雜演算法)
- 混合型任務 = 視機器配置和複雜度自測而定
定義Executor:
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, corePoolSize+1, 10l, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(1000));
- corePoolSize用於指定核心線程數量
- maximumPoolSize指定最大線程數
- keepAliveTime和TimeUnit指定線程空閑後的最大存活時間
workQueue則是線程池的緩衝隊列,還未執行的線程會在隊列中等待,監控隊列長度,確保隊列有界;不當的線程池大小會使得處理速度變慢,穩定性下降,並且導致記憶體泄露。如果配置的線程過少,則隊列會持續變大,消耗過多記憶體;而過多的線程又會 由於頻繁的上下文切換導致整個系統的速度變緩——殊途而同歸。隊列的長度至關重要,它必須得是有界的,這樣如果線程池不堪重負了它可以暫時拒絕掉新的請求。
ExecutorService 預設的實現是一個無界的LinkedBlockingQueue。
Tomcat線程池
以上只是為了測試方便,模擬出的數據。真實的生產環境,我們要接入Nginx和Tomcat來處理用戶的請求。而Tomcat作為一名容器也是有自己的一套連接池的,作為開發人員你並不需要自己去實現。
Tomcat預設使用自帶的連接池,這裡我們也可以自定義實現,打開/conf/server.xml文件,在Connector之前配置一個線程池:
<Executor name="tomcatThreadPool"
namePrefix="tomcatThreadPool-"
maxThreads="1000"
maxIdleTime="300000"
minSpareThreads="200"/>
name:共用線程池的名字。這是Connector為了共用線程池要引用的名字,該名字必須唯一。預設值:None;
namePrefix:在JVM上,每個運行線程都可以有一個name 字元串。這一屬性為線程池中每個線程的name字元串設置了一個首碼,Tomcat將把線程號追加到這一首碼的後面。預設值:tomcat-exec-;
maxThreads:該線程池可以容納的最大線程數。預設值:200;
maxIdleTime:在Tomcat關閉一個空閑線程之前,允許空閑線程持續的時間(以毫秒為單位)。只有當前活躍的線程數大於minSpareThread的值,才會關閉空閑線程。預設值:60000(一分鐘)。
minSpareThreads:Tomcat應該始終打開的最小不活躍線程數。預設值:25。
配置Connector:
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
minProcessors="5"
maxProcessors="75"
acceptCount="1000"/>
executor:表示使用該參數值對應的線程池;
minProcessors:伺服器啟動時創建的處理請求的線程數;
maxProcessors:最大可以創建的處理請求的線程數;
acceptCount:指定當所有可以使用的處理請求的線程數都被使用時,可以放到處理隊列中的請求數,超過這個數的請求將不予處理。
思考
- 為什麼線程數最好不要太大於CPU核數?
- 為什麼Tomcat中預設線程數遠大於CPU核數?
- Nginx為什麼要進入線程池,基於什麼場景考慮?
代碼案例:從0到1構建分散式秒殺系統