螞蟻金服后端一面:Java怎么使用線程池/理解/處理什么問(wèn)題
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)
真香!24W字的Java面試手冊(cè)(點(diǎn)擊查看)
什么是線程池呢,字面意思,池是一個(gè)容器,比如我們之前還學(xué)過(guò)JDBC的連接池,還有內(nèi)存池,對(duì)象池等等。
線程池就是事先準(zhǔn)備一些線程資源,需要的時(shí)候拿去用,用完的時(shí)候還回來(lái)。
在之前有個(gè)推文講了SpringBoot中集成的線程池用法
線程池好處
每次創(chuàng)建線程和銷(xiāo)毀線程都是消耗一定的資源,將線程統(tǒng)一進(jìn)行管理,可以大大減少了創(chuàng)建/銷(xiāo)毀的次數(shù),而且還能提高響應(yīng)的速度。另外多個(gè)線程去爭(zhēng)奪CPU資源時(shí)會(huì)造成堵塞,線程池可以統(tǒng)一對(duì)資源進(jìn)行分配,提高了線程的穩(wěn)定性和可控制性。
總結(jié):
每個(gè)線程都可以被重復(fù)利用,減少了創(chuàng)建和銷(xiāo)毀線程的次數(shù)。降低資源消耗 靈活的調(diào)整線程的數(shù)量,方便管理 提高響應(yīng)速度
在面試中,經(jīng)常會(huì)問(wèn)到線程池的 四大方法、七個(gè)參數(shù)、四種拒絕策略
四大方法
即創(chuàng)建線程池的四種方法,如何創(chuàng)建線程池呢,很多人使用Executors來(lái)創(chuàng)建線程池,比如
ExecutorService threadPool = Executors.newFixedThreadPool(5);
創(chuàng)建線程有四種方法
newFixedThreadPool:創(chuàng)建固定大小的線程池
newSingleThreadExecutor:創(chuàng)建只有一個(gè)線程的線程池,確保任務(wù)串行執(zhí)行
newCachedThreadPool:創(chuàng)建一個(gè)不限制線程數(shù)的線程池。如果當(dāng)前線程池的規(guī)模超出了處理需求,將回收空的線程;當(dāng)需求增加時(shí),會(huì)增加線程數(shù)量;線程池規(guī)模無(wú)限制。
newScheduledThreadPool(不是很常用,適用于特定場(chǎng)景):創(chuàng)建一個(gè)固定長(zhǎng)度的線程池,而且以延遲或者定時(shí)的方式來(lái)執(zhí)行
四個(gè)方法的源碼如下。
// 1
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 2
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
// 3
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 4
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
從源碼中可以看到除了最后一個(gè)newScheduledThreadPool,其余的都是 return 了一個(gè)ThreadPoolExecutor對(duì)象。
所以,開(kāi)啟線程池的本質(zhì)就是調(diào)用了ThreadPoolExecutor方法
在阿里巴巴開(kāi)發(fā)手冊(cè)中明確規(guī)定:
“【強(qiáng)制】線程池不允許使用 Executors 去創(chuàng)建,而是通過(guò) ThreadPoolExecutor 的方式,這 樣的處理方式讓寫(xiě)的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。說(shuō)明:Executors 返回的線程池對(duì)象的弊端如下:
FixedThreadPool 和 SingleThreadPool: 允許的請(qǐng)求隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致 OOM。
CachedThreadPool: 允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致 OOM。
”
如果你的IDE安裝了阿里巴巴編碼規(guī)約的插件你會(huì)發(fā)現(xiàn)會(huì)有這樣的提示
因此創(chuàng)建線程正確的方法是使用ThreadPoolExecutor來(lái)手動(dòng)創(chuàng)建(newScheduledThreadPool除外),這樣可以更好的規(guī)避資源耗盡的風(fēng)險(xiǎn)。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
從上面看,ThreadPoolExecutor有7個(gè)參數(shù),對(duì)應(yīng)的含義如下:
int corePoolSize:核心線程池大小int maximumPoolSize:最大線程池大小long keepAliveTime:線程最大的空閑時(shí)間(標(biāo)記線程空閑多久釋放)TimeUnit unit:keepAliveTime的時(shí)間單位BlockingQueue wordQueue:阻塞隊(duì)列ThreadFactory threadFactory:線程的創(chuàng)建工廠,一般不用動(dòng)RejectedExecutionHandle:拒絕策略
newSingleThreadExecutor(創(chuàng)建只有一個(gè)線程的線程池) 默認(rèn)核心線程池大小是1,最大線程池也是1,而創(chuàng)建固定大小線程池的newFixedThreadPool與newSingleThreadExecutor基本一樣,只不過(guò)把線程池大小和最大線程池大小動(dòng)態(tài)化了。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,....);
}
// 2
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,..,threadFactory));
}
而newCachedThreadPool,核心線程池大小是0,但是默認(rèn)最大線程池大小是Integer.MAX_VALUE ,這是一個(gè)億級(jí)的數(shù)值,如果一個(gè)服務(wù)器上有上億個(gè)線程那消耗的資源必然是非常大的,因此正如前面講到的阿里開(kāi)發(fā)規(guī)范中建議通過(guò)ThreadPoolExecutor來(lái)手動(dòng)創(chuàng)建線程,指定相應(yīng)的參數(shù),避免發(fā)生OOM。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());}
舉個(gè)??
咱們?nèi)ャy行辦理業(yè)務(wù)。先抽號(hào),然后在候客區(qū)等著叫號(hào),假設(shè)有6個(gè)窗口,因?yàn)榻裉烊瞬皇呛芏?,其?個(gè)窗口關(guān)閉了,只開(kāi)了2個(gè)辦理業(yè)務(wù)的窗口
關(guān)于核心線程池/ 最大線程池/ 空閑時(shí)間 / 阻塞隊(duì)列 / 拒絕策略 ,下圖應(yīng)該很形象了。
拒絕策略:
查看源代碼發(fā)現(xiàn) RejectedExecutionHandler 有四種實(shí)現(xiàn),也就是四種拒絕策略。
四種拒絕策略分別有什么用呢?還是從源碼上來(lái)看。
AbortPolicy
直接throw 一個(gè)異常,不做處理
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
CallerRunsPolicy
如果添加線程池失敗,就把任務(wù)交給主線程去執(zhí)行。好比排隊(duì)領(lǐng)妹子,你不讓我排隊(duì),那我自己去找妹子.
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
DiscardOldestPolicy
如果阻塞隊(duì)列滿了,就把最開(kāi)始加入隊(duì)列的任務(wù)移除出去。再?lài)L試加入隊(duì)列。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
DiscardPolicy
不做任何處理,rejectedExecution是一個(gè)空方法
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
ok!每個(gè)參數(shù)都搞明白了,用代碼實(shí)現(xiàn)一下上面的去銀行辦業(yè)務(wù)的例子
package bilibili;
import java.awt.desktop.AboutHandler;
import java.util.concurrent.*;
public class Pool {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, // 核心線程2
6, // 最大線程6
3, // 最大空閑時(shí)間3秒
TimeUnit.SECONDS, // 時(shí)間單位秒
new LinkedBlockingDeque<>(4), // 阻塞隊(duì)列,容量4
Executors.defaultThreadFactory(), // 默認(rèn)的創(chuàng)建工廠
new ThreadPoolExecutor.AbortPolicy() //AbortPolicy拒絕策略,拋出異常
);
try {
for (int i = 0; i < 5; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("正在執(zhí)行: " + Thread.currentThread().getName());
}
});
}
} catch (Exception e) {
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
運(yùn)行結(jié)果
正在執(zhí)行: pool-1-thread-2
正在執(zhí)行: pool-1-thread-1
正在執(zhí)行: pool-1-thread-2
正在執(zhí)行: pool-1-thread-2
正在執(zhí)行: pool-1-thread-1
可以看到在for循環(huán)中i < 5,沒(méi)有超過(guò)最大線程,此時(shí)使用的是 2 個(gè)核心線程在執(zhí)行
當(dāng) i < 7,大于總線程數(shù),但是小于總線程+ 阻塞線程(6 + 4)
此時(shí),使用了pool-1-thread-3線程
如果正好10個(gè)線程,也就是最大線程數(shù)6 + 阻塞線程4。剛好夠用,
上面寫(xiě)法過(guò)于臃腫,這里使用函數(shù)式編程的方法
//其他代碼省略
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(()-> {
System.out.println("正在執(zhí)行: " + Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
}finally {
threadPool.shutdown();
}
執(zhí)行結(jié)果:
可以看到線程池中的 6 個(gè)線程都被用到了
正在執(zhí)行: pool-1-thread-4
正在執(zhí)行: pool-1-thread-4
正在執(zhí)行: pool-1-thread-4
正在執(zhí)行: pool-1-thread-4
正在執(zhí)行: pool-1-thread-4
正在執(zhí)行: pool-1-thread-1
正在執(zhí)行: pool-1-thread-2
正在執(zhí)行: pool-1-thread-5
正在執(zhí)行: pool-1-thread-6
正在執(zhí)行: pool-1-thread-3
但是!當(dāng)循環(huán)11次,也就是11個(gè)人來(lái)辦理業(yè)務(wù)時(shí),因?yàn)槌^(guò)了最大的線程數(shù) + 阻塞線程,就會(huì)觸發(fā) 拒絕策略。
因?yàn)樵诙x的時(shí)候使用的是AbortPolicy,因此會(huì)拋出異常。
如果此時(shí)拒絕策略,改為CallerRunsPolicy, 也就是說(shuō)多出來(lái)的一個(gè)人交給主線程去執(zhí)行。
拒絕策略為DiscardOldestPolicy或者DiscardPolicy時(shí),控制臺(tái)只會(huì)輸出10條結(jié)果。區(qū)別在于DiscardOldestPolicy會(huì)把最開(kāi)始加入隊(duì)列的任務(wù)移除出去,嘗試去競(jìng)爭(zhēng)線程池資源。而DiscardPolicy則將最后多出來(lái)的那一個(gè)不做任何處理。被干掉了。
本文主要講了線程池的作用,以及三種創(chuàng)建方法,7個(gè)參數(shù),4種拒絕策略。這在面試中是經(jīng)常問(wèn)到的。如有不對(duì)之處歡迎指出。
如有文章對(duì)你有幫助,
歡迎關(guān)注??、點(diǎn)贊??、轉(zhuǎn)發(fā)??!
推薦, Java面試手冊(cè) 內(nèi)容包括網(wǎng)絡(luò)協(xié)議、Java基礎(chǔ)、進(jìn)階、字符串、集合、并發(fā)、JVM、數(shù)據(jù)結(jié)構(gòu)、算法、MySQL、Redis、Mongo、Spring、SpringBoot、MyBatis、SpringCloud、Linux以及各種中間件(Dubbo、Nginx、Zookeeper、MQ、Kafka、ElasticSearch)等等... 點(diǎn)擊文末“閱讀原文”可直達(dá)





