色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

線程池三大核心參數的含義 線程池核心線程數制定策略

jf_ro2CN3Fa ? 來源:稀土掘金技術社區 ? 2023-12-01 10:20 ? 次閱讀

1 前言

說到線程池八股文背的很熟的肯定知道無非就這幾個考點:

(1)線程池三大核心參數 corePoolSize、maximumPoolSize、workQueue 的含義

(2)線程池核心線程數制定策略

(3)建議通過 ThreadPoolExecutor 的構造函數來聲明,避免使用 Executors 創建線程池

以上考點作為線程池面試幾乎必問的內容,大部分人應該都是如數家珍,張口就來,但是懂了面試八股文真的就不一定在實際運用中真的就會把線程池用好 。且看下面這次真實生產事故還原

2 事故還原

某次一位研發同事寫出了下面類似的代碼:

Listitems=getFromDb();
List>completableFutures=items.stream().map(item->CompletableFuture.supplyAsync(()->{
AppMapStationDatadata=mapper.copy(item);
//發起價格信息查詢的RPC調用
data.setPriceInfo(priceApi.getPriceInfoById(item.getId()))
returndata;
},apiExecutor)).collect(Collectors.toList());

result=completableFutures.stream().map(e->{
returne.get();
}).filter(Objects::nonNull).collect(Collectors.toList());

上面的代碼中,代碼首先從數據庫里面查出來一堆對象,然后對每一個對象進行模型轉換,由于要獲取每個對象的價格信息發起了一次RPC調用,由于RPC服務沒有提供批量接口,所以代碼里面用了線程池并發請求,以求得接口盡可能快的返回數據。

使用的是CompletableFuture 而且自定義了線程池,線程池指定了10個核心線程,20個最大線程,這段代碼在上線后的一段時間確實沒有任何問題,但是在灰度放量用戶量多起來之后發現接口經常超時告警。

請問為什么上面的代碼在用戶量稍微大一點的時候就運行緩慢了呢?

實際代碼問題出現在了這個get方法中,這個get方法沒有指定超時時間,當getPriceInfoById這個接口響應變慢的時候,這個主線程的代碼get又沒有指定超時時間,這時候問題就來了。

由于某次業務查詢查到了非常多的數據,每條數據就是個模型轉換任務,這個任務就會在隊列排隊,get方法沒有指定超時時間的情況下,其最終耗時就取決于整個線程池中執行最慢的那一個任務,所以當從DB中查出來的數據量越來越大的時候這個轉換任務的最大耗時就會逐漸增加,進而引發接口超時。

所以這里改進上述問題需要做到兩個點:

1、數據庫中查出來的數據集合必須分頁

2、get方法必須設置超時時間

此外需要知道get方法設置超時時間的計算方式也需要留意,考慮下面這種場景

提交兩個任務 A 和 B 到線程池,A 任務耗時 3 秒,B 任務耗時 4 秒,Future 以 2 秒為超時時間獲取任務結果

代碼如下:

ExecutorServiceexecutorService=Executors.newFixedThreadPool(2);

CallabletaskA=()->{
sleep(3);
return"A";
};
CallabletaskB=()->{
sleep(4);
return"B";
};

List>futures=Stream.of(taskA,taskB)
.map(executorService::submit)
.collect(Collectors.toList());

for(Futurefuture:futures){
try{
Strings=future.get(2,TimeUnit.SECONDS);
System.out.println(s);
}catch(Exceptione){
continue;
}
}

實際運行情況是第一個任務會超時但是第二個不會 ,看起來是不是還有點不可思議,耗時時間長的任務B反而沒超時。原因就在于 Future.get(long timeout, TimeUnit unit) ,調用 get 時才開始計時,而非任務加入線程池的時間

dbba0046-8fe9-11ee-939d-92fbcf53809c.jpg

從圖上就可以看出來,在獲取B的任務執行結果的時候B任務已經執行了兩秒,所以在等待兩秒的情況下可以獲取到結果

3 線程池不當使用舉例

(1)不區分業務一把梭哈,全用一個線程池

曾經有一個項目,對接多個租戶,每個租戶都有各自的任務需要執行,代碼中不區分租戶的將所有租戶的任務全部丟到一個線程池中執行,結果一個租戶的任務提交過多導致線程池執行緩慢,但是由于線程池是同一個,影響了所有租戶接口的響應時間。如果說上面說的這個場景用一個線程池產生了租戶互相影響的問題還不夠嚴重,那么下面的這種場景就問題更大了。

曾經有一段這樣的場景,因為共用線程池直接導致線程池任務永遠完成不了,請看下面的這種情況:

首先向線程池中提交了一個任務,然后在這個任務的內部實現中又往同一個線程池中再次提交了一個任務,相當于父子任務在同一個線程池中執行,這時候極有可出現線程死鎖也就是循環等待的情況

dbcc942c-8fe9-11ee-939d-92fbcf53809c.jpg

如上圖所示,父任務全部處于執行狀態,這時候子任務想要執行需要等父任務執行完成,但是父任務都執行不完,因為還有個子任務沒完成,即父任務等待子任務執行完成,而子任務等待父任務釋放線程池資源,這也就造成了 "死鎖"

所以綜上所述,在代碼中應該避免各種任務都往一個線程池中投放,對每個線程池指定好線程名稱,做好分類比較合適,這里在日常開發中比較推薦使用Guava的工具類,來指定線程名稱前綴,這樣使用jstack分析線程問題也方便排查。

ThreadFactorythreadFactory=newThreadFactoryBuilder()
.setNameFormat(threadNamePrefix+"-%d")
.setDaemon(true).build();
ExecutorServicethreadPool=newThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.MINUTES,
workQueue,
threadFactory);

(2)@Async注解不自己定義線程池

@Async用在方法上標識這是一個異步方法,如果不自己指定線程池這個方法將直接新建一個線程執行,可以翻看spring實現源碼知道這個點

@Async的實現其實非常簡單就是利用AOP,容器啟動的時候會掃描所有被打上@Async注解的方法,并代理這些方法的執行,在執行這個方法的時候,生成Callable任務丟到線程池中執行(核心代碼位于org.springframework.aop.interceptor.AsyncExecutionInterceptor)

@Override
@Nullable
publicObjectinvoke(finalMethodInvocationinvocation)throwsThrowable{
ClasstargetClass=(invocation.getThis()!=null?AopUtils.getTargetClass(invocation.getThis()):null);
MethodspecificMethod=ClassUtils.getMostSpecificMethod(invocation.getMethod(),targetClass);
finalMethoduserDeclaredMethod=BridgeMethodResolver.findBridgedMethod(specificMethod);

AsyncTaskExecutorexecutor=determineAsyncExecutor(userDeclaredMethod);
if(executor==null){
thrownewIllegalStateException(
"NoexecutorspecifiedandnodefaultexecutorsetonAsyncExecutionInterceptoreither");
}
//將方法調用封裝成Callable實例丟入線程池中執行
Callabletask=()->{
try{
Objectresult=invocation.proceed();
if(resultinstanceofFuture){
return((Future)result).get();
}
}
catch(ExecutionExceptionex){
handleError(ex.getCause(),userDeclaredMethod,invocation.getArguments());
}
catch(Throwableex){
handleError(ex,userDeclaredMethod,invocation.getArguments());
}
returnnull;
};

returndoSubmit(task,executor,invocation.getMethod().getReturnType());
}

如果不指定線程池這里就會啟用默認的線程池 SimpleAsyncTaskExecutor 然后我們看下這個類的注釋

dbddedee-8fe9-11ee-939d-92fbcf53809c.jpg

那這個問題就很嚴重了,假定你的方法執行速度慢,而且qps大,這時候線程數就會直接爆炸,所以建議寫一個類繼承 AsyncConfigurer接口并復寫getAsyncExecutor方法,然后在使用注解的時候指定線程池的名稱

//使用注解時指定線程池的Bean名稱
@Async("apiExecutor")

(3)線程池遇上ThreadLocal

線程池和 ThreadLocal 共用,可能會導致線程從 ThreadLocal 獲取到的是舊值/臟數據。這是因為線程池會復用線程對象,與線程對象綁定的類的靜態屬性 ThreadLocal 變量也會被重用,這就導致一個線程可能獲取到其他線程的 ThreadLocal 值。

比較常規的做法是在任務執行完畢之后的finally代碼塊里面做清理工作

Runnablerunnable=()->{
try{
BizThreadLocal.set("xxxx");
//dosth
}finally{
BizThreadLocal.remove();
}
};

但是其實finally的代碼塊其實也不是百分百一定執行,事實上Thread#stop() 方法打斷線程執行的時候 finally代碼塊中的內容就不會執行,比較推薦的還是# TransmittableThreadLocal

4 再談線程池,幾個關鍵要點

(1)為什么默認線程池的隊列長度不能動態調整?

曾經面對生產環境線程池的參數設定問題,我曾經想到一個方案,既然線程池的參數不好定,那咱們直接動態修改就行不行呢,線程池本身提供了很多的set方法可以做到參數修改,比如我們在springBoot項目往往去使用ThreadPoolTaskExecutor 作為線程池,從下圖的set方法列表中可以看出存在很多修改線程池參數的方法

dbfd781c-8fe9-11ee-939d-92fbcf53809c.jpg

然后實際使用的時候發現核心線程數和最大線程數都能動態修改 但是隊列長度卻不能 ,為什么隊列長度不能調用setQueueCapacity方法進行動態修改呢?

首先我們可以簡單理解為spring的ThreadPoolTaskExecutor是Java原生ThreadPoolExecutor的封裝,觀察這個類的setMaxPoolSize和setQueueCapacity代碼實現我們就能發現setQueueCapacity 實際就是一個賦值僅在第一次實例化線程池的使用到了這個參數。

publicvoidsetMaxPoolSize(intmaxPoolSize){
synchronized(this.poolSizeMonitor){
if(this.threadPoolExecutor!=null){
this.threadPoolExecutor.setMaximumPoolSize(maxPoolSize);
}

this.maxPoolSize=maxPoolSize;
}
}

publicvoidsetQueueCapacity(intqueueCapacity){
this.queueCapacity=queueCapacity;
}


protectedBlockingQueuecreateQueue(intqueueCapacity){
return(BlockingQueue)(queueCapacity>0?newLinkedBlockingQueue(queueCapacity):newSynchronousQueue());
}

從上面的源碼我們還可以看出spring的ThreadPoolTaskExecutor使用的隊列是LinkedBlockingQueue ,那么為啥線程池ThreadPoolExecutor不支持修改隊列長度呢?這個原因就很簡單了因為這個隊列的capacity是final類型的,自然不能修改。

dc0e7284-8fe9-11ee-939d-92fbcf53809c.jpg

那如果我一定要修改這個隊列長度應該怎么處理?那完全就可以仿照美團的方式,自定義了一個叫做 ResizableCapacityLinkedBlockIngQueue 的隊列(把LinkedBlockingQueue的capacity 字段的final關鍵字修飾給去掉了,讓它變為可變的)是不是也是很簡單。

(2)再談核心線程數的參數設置

核心線程池的參數設置一般各種網絡資料中比較推崇的是N+1和2N法,即:

CPU 密集型任務(N+1) :這種任務消耗的主要是 CPU 資源,可以將線程數設置為 N(CPU 核心數)+1。比 CPU 核心數多出來的一個線程是為了防止線程偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處于空閑狀態,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。

I/O 密集型任務(2N) :這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會占用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。

如何判斷是 CPU 密集任務還是 IO 密集任務?

CPU 密集型 :簡單理解就是利用 CPU 計算能力的任務比如你在內存中對大量數據進行排序。

IO 密集型 :涉及到網絡讀取,文件讀取這類都是 IO 密集型,這類任務的特點是 CPU 計算耗費時間相比于等待 IO 操作完成的時間來說很少,大部分時間都花在了等待 IO 操作完成上。

但是實際上比較科學的線程數計算方式是:

最佳線程數 = N(CPU 核心數)?(1+WT(線程等待時間)/ST(線程計算時間))

WT(線程等待時間)= 線程運行總時間 - ST(線程計算時間)

線程等待時間所占比例越高,需要越多線程。線程計算時間所占比例越高,需要越少線程。(我們可以通過 JDK 自帶的工具 VisualVM 來查看 WT/ST 比例)

CPU 密集型任務的 WT/ST 接近或者等于 0,因此, 線程數可以設置為 N(CPU 核心數)?(1+0)= N,和我們上面說的 N(CPU 核心數)+1 差不多。IO 密集型任務下,幾乎全是線程等待時間,從理論上來說,你就可以將線程數設置為 2N。

這里額外說一句早先我雖然知道線程池核心線程數應該和CPU核心線程數有關,但是悲劇的是我并不知道怎么查Linux系統的核心數,這里把查詢命令貼出來供參考:

#總核數=物理CPU個數X每顆物理CPU的核數
#查看物理CPU個數
cat/proc/cpuinfo|grep"physicalid"|sort|uniq|wc-l

#查看每個物理CPU中core的個數(即核數)
cat/proc/cpuinfo|grep"cpucores"|uniq

(3)為什么不太推薦你用ParallelStream?

曾經某次代碼評審中,有位同學寫出了下面類似的代碼

//從數據庫中查找學生對象
Liststudents=searchDataFromDb();

//使用并行流進行模型轉換
Listres=newArrayList();
students.parallelStream().forEach(student->{
StudentVovo=newStudentVo(student)
res.add(student);
});

結果測試過程中返回給前端的數據總是莫名其妙的少很多和數據庫中的真實數據條數對不上,相信大家都看出來了原因是List并不是線程安全的容器,所以導致了最后結果不對,其實這不能算是parallelStream的問題,但是很多人寫代碼時,以為并行流就快為了追求效率,不假思索就寫了這樣的代碼,但是往往在線程池的環境下大家又仿佛繃緊了并發神經,又能考慮到了并發問題。

此外parallelStream的默認線程池遇上ThreadLocal同樣也存在一些問題,其實如果不做額外線程池指定,代碼中的 parallelStream 都是共用同一個線程池的,ParallelStream 底層使用了 ForkJoinPool,當 Stream 流中元素較多時,整個運行效率也會大大降低。

5 總結

本文通過一次生產事故,進一步總結了線程池在日常開發中需要注意的一些要點,希望對大家有所幫助。

審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 函數
    +關注

    關注

    3

    文章

    4327

    瀏覽量

    62569
  • 代碼
    +關注

    關注

    30

    文章

    4779

    瀏覽量

    68521
  • 線程池
    +關注

    關注

    0

    文章

    57

    瀏覽量

    6844

原文標題:【避坑】線程池沒用好,直接出現了生產事故....

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    跨平臺的線程組件--TP組件

    問題產生 無論是Linux,RTOS,還是Android等開發,我們都會用到多線程編程;但是往往很多人在編程時,都很隨意的創建/銷毀線程策略來實現多線程編程;很明顯這是不合理的做法,
    的頭像 發表于 04-06 15:39 ?871次閱讀

    Java中的線程包括哪些

    線程是用來統一管理線程的,在 Java 中創建和銷毀線程都是一件消耗資源的事情,線程可以重復
    的頭像 發表于 10-11 15:33 ?809次閱讀
    Java中的<b class='flag-5'>線程</b><b class='flag-5'>池</b>包括哪些

    線程是如何實現的

    線程的概念是什么?線程是如何實現的?
    發表于 02-28 06:20

    基于Nacos的簡單動態化線程實現

    本文以Nacos作為服務配置中心,以修改線程核心線程數、最大線程數為例,實現一個簡單的動態化線程
    發表于 01-06 14:14 ?862次閱讀

    線程線程

    線程通常用于服務器應用程序。 每個傳入請求都將分配給線程池中的一個線程,因此可以異步處理請求,而不會占用主線程,也不會延遲后續請求的處理
    的頭像 發表于 02-28 09:53 ?786次閱讀
    多<b class='flag-5'>線程</b>之<b class='flag-5'>線程</b><b class='flag-5'>池</b>

    Java線程核心原理

    看過Java線程源碼的小伙伴都知道,在Java線程池中最核心的類就是ThreadPoolExecutor,
    的頭像 發表于 04-21 10:24 ?850次閱讀

    核心線程數和最大線程數區別

    達到最大線程數。當任務執行完畢后,線程會根據線程參數來決定是否回收
    的頭像 發表于 06-01 09:33 ?7652次閱讀

    線程線程怎么釋放

    線程分組看,pool名開頭線程占616條,而且waiting狀態也是616條,這個點就非常可疑了,我斷定就是這個pool開頭線程導致的問題。我們先排查為何這個
    發表于 07-31 10:49 ?2274次閱讀
    <b class='flag-5'>線程</b><b class='flag-5'>池</b>的<b class='flag-5'>線程</b>怎么釋放

    Spring 的線程應用

    我們在日常開發中,經常跟多線程打交道,Spring 為我們提供了一個線程方便我們開發,它就是 ThreadPoolTaskExecutor ,接下來我們就來聊聊 Spring 的線程
    的頭像 發表于 10-13 10:47 ?620次閱讀
    Spring 的<b class='flag-5'>線程</b><b class='flag-5'>池</b>應用

    線程基本概念與原理

    一、線程基本概念與原理 1.1 線程概念及優勢 C++線程簡介
    的頭像 發表于 11-10 10:24 ?528次閱讀

    線程的基本概念

    線程的基本概念 不管線程是什么東西!但是我們必須知道線程被搞出來的目的就是:提高程序執行效
    的頭像 發表于 11-10 16:37 ?520次閱讀
    <b class='flag-5'>線程</b><b class='flag-5'>池</b>的基本概念

    核心線程數和最大線程數怎么設置

    核心線程數和最大線程數是Java線程池中重要的參數,用來控制線程池中
    的頭像 發表于 12-01 13:50 ?9041次閱讀

    線程七大核心參數執行順序

    線程是一種用于管理和調度線程執行的技術,通過將任務分配到線程池中的線程進行處理,可以有效地控制并發線程
    的頭像 發表于 12-04 16:45 ?1052次閱讀

    線程的創建方式有幾種

    的開銷。線程的創建方式有多種,下面將詳細介紹幾種常用的線程創建方式。 手動創建線程 手動創
    的頭像 發表于 12-04 16:52 ?854次閱讀

    什么是動態線程?動態線程的簡單實現思路

    因此,動態可監控線程一種針對以上痛點開發的線程管理工具。主要可實現功能有:提供對 Spring 應用內線程
    的頭像 發表于 02-28 10:42 ?639次閱讀
    主站蜘蛛池模板: 穿白丝袜边走边尿白丝袜| 免费果冻传媒2021视频| 99精品99| 亚洲人成人毛片无遮挡| 全黄H全肉细节文NP| 久久精品国产在热亚洲完整版| 草莓湿漉漉是好事还是恶性| 亚洲欧美日韩人成| 少妇大荫蒂毛多毛大| 免费看到湿的小黄文软件APP| 国产在线AV一区二区香蕉| 扒开老师大腿猛进AAA片| 在线观看国产区| 亚洲成人黄色在线| 色姐妹久久综合在线av| 女人高潮久久久叫人喷水| 久久精品国产亚洲AV影院| 国产免费久久精品国产传媒| 处初女处夜情视频在线播放| 99re8久久热在线视频| 正在播放一区二区| 野花社区WWW韩国日本| 午夜免费国产体验区免费的| 日本片bbbxxx| 彭丹吃奶门| 暖暖视频免费观看高清完整版 | 久久丫线这里只精品| 国产亚洲精品品视频在线 | 欧美互交人妖247| 麻豆精品传媒一二三区| 久久精品动漫99精品动漫| 狠狠插综合| 久久精品亚洲牛牛影视| 公和熄洗澡三级中文字幕| 99热都是精品| a三级黄色片| 99re1久久热在线播放| 97视频久久| 99久久精品免费看国产免费| 99精品免费久久久久久久久日本| 521人成a天堂v|