前言
從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構建分散式秒殺系統