前言
多線程的軟件設(shè)計(jì)方法確實(shí)可以最大限度的發(fā)揮現(xiàn)代多核處理器的計(jì)算能力幢哨,提高生產(chǎn)系統(tǒng)的吞吐量和性能雄人。但是,如果一個(gè)系統(tǒng)同時(shí)創(chuàng)建大量線程矩距,線程間頻繁的切換上下文導(dǎo)致的系統(tǒng)開銷將會(huì)拖慢整個(gè)系統(tǒng)拗盒。嚴(yán)重的甚至導(dǎo)致內(nèi)存耗盡導(dǎo)致OOM異常。因此锥债,在實(shí)際的生產(chǎn)環(huán)境中陡蝇,線程的數(shù)量必須得到控制痊臭,盲目的創(chuàng)建大量新車對(duì)系統(tǒng)是有傷害的。
那么登夫,怎么才能最大限度的利用CPU的性能广匙,又能保持系統(tǒng)的穩(wěn)定性呢?其中有一個(gè)方法就是使用線程池恼策。
簡(jiǎn)而言之鸦致,在使用線程池后,創(chuàng)建線程便處理從線程池獲得空閑線程涣楷,關(guān)閉線程變成了向池子歸還線程分唾。也就是說,提高了線程的復(fù)用狮斗。
而 JDK 在 1.5 之后為我提供了現(xiàn)成的線程池工具鳍寂,我們今天就來學(xué)習(xí)看看如何使用他們。
- Executors 線程池工廠能創(chuàng)建哪些線程池
- 如何手動(dòng)創(chuàng)建線程池
- 如何擴(kuò)展線程池
- 如何優(yōu)化線程池的異常信息
- 如何設(shè)計(jì)線程池中的線程數(shù)量
1. Executors 線程池工廠能創(chuàng)建哪些線程池
先來一個(gè)最簡(jiǎn)單的線程池使用例子:
static class MyTask implements Runnable {
@Override
public void run() {
System.out
.println(System.currentTimeMillis() + ": Thread ID :" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyTask myTask = new MyTask();
ExecutorService service1 = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
service1.submit(myTask);
}
service1.shutdown();
}
運(yùn)行結(jié)果:
我們創(chuàng)建了一個(gè)線程池實(shí)例情龄,并設(shè)置默認(rèn)線程數(shù)量為5,并向線程池提交了10任務(wù)捍壤,分別打印當(dāng)前毫秒時(shí)間和線程ID骤视,從結(jié)果中,我們可以看到結(jié)果中有5個(gè)相同 id 的線程打印了毫秒時(shí)間鹃觉。
這是最簡(jiǎn)單的例子专酗。
接下來我們講講其他的線程創(chuàng)建方式。
1. 固定線程池
ExecutorService service1 = Executors.newFixedThreadPool(5);
該方法返回一個(gè)固定線程數(shù)量的線程池盗扇。該線程池中的線程數(shù)量始終不變祷肯。當(dāng)有一個(gè)新的任務(wù)提交時(shí),線程池中若有空閑線程疗隶,則立即執(zhí)行佑笋,若沒有,則新的任務(wù)會(huì)被暫存在一個(gè)任務(wù)隊(duì)列(默認(rèn)無界隊(duì)列 int 最大數(shù))中斑鼻,待有線程空閑時(shí)蒋纬,便處理在任務(wù)隊(duì)列中的任務(wù)。
2. 單例線程池
ExecutorService service3 = Executors.newSingleThreadExecutor();
該方法返回一個(gè)只有一個(gè)線程的線程池坚弱。若多余一個(gè)任務(wù)被提交到該線程池蜀备,任務(wù)會(huì)被保存在一個(gè)任務(wù)隊(duì)列(默認(rèn)無界隊(duì)列 int 最大數(shù))中,待線程空閑荒叶,按先入先出的順序執(zhí)行隊(duì)列中的任務(wù)碾阁。
3. 緩存線程池
ExecutorService service2 = Executors.newCachedThreadPool();
該方法返回一個(gè)可根據(jù)實(shí)際情況調(diào)整線程數(shù)量的線程池,線程池的線程數(shù)量不確定些楣,但若有空閑線程可以復(fù)用脂凶,則會(huì)優(yōu)先使用可復(fù)用的線程宪睹,所有線程均在工作,如果有新的任務(wù)提交艰猬,則會(huì)創(chuàng)建新的線程處理任務(wù)横堡。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后,將返回線程池進(jìn)行復(fù)用冠桃。
4. 任務(wù)調(diào)用線程池
ExecutorService service4 = Executors.newScheduledThreadPool(2);
該方法也返回一個(gè) ScheduledThreadPoolExecutor 對(duì)象命贴,該線程池可以指定線程數(shù)量。
前3個(gè)線程的用法沒什么差異食听,關(guān)鍵是第四個(gè)胸蛛,雖然線程任務(wù)調(diào)度框架很多,但是我們?nèi)匀豢梢詫W(xué)習(xí)該線程池樱报。如何使用呢葬项?下面來個(gè)例子:
class A {
public static void main(String[] args) {
ScheduledThreadPoolExecutor service4 = (ScheduledThreadPoolExecutor) Executors
.newScheduledThreadPool(2);
// 如果前面的任務(wù)沒有完成,則調(diào)度也不會(huì)啟動(dòng)
service4.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
// 如果任務(wù)執(zhí)行時(shí)間大于間隔時(shí)間迹蛤,那么就以執(zhí)行時(shí)間為準(zhǔn)(防止任務(wù)出現(xiàn)堆疊)民珍。
Thread.sleep(10000);
System.out.println(System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}// initialDelay(初始延遲) 表示第一次延時(shí)時(shí)間 ; period 表示間隔時(shí)間
}, 0, 2, TimeUnit.SECONDS);
service4.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println(System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}// initialDelay(初始延遲) 表示延時(shí)時(shí)間;delay + 任務(wù)執(zhí)行時(shí)間 = 等于間隔時(shí)間 period
}, 0, 2, TimeUnit.SECONDS);
// 在給定時(shí)間盗飒,對(duì)任務(wù)進(jìn)行一次調(diào)度
service4.schedule(new Runnable() {
@Override
public void run() {
System.out.println("5 秒之后執(zhí)行 schedule");
}
}, 5, TimeUnit.SECONDS);
}
}
}
上面的代碼創(chuàng)建了一個(gè) ScheduledThreadPoolExecutor 任務(wù)調(diào)度線程池嚷量,分別調(diào)用了3個(gè)方法,需要著重解釋 scheduleAtFixedRate 和 scheduleWithFixedDelay 方法逆趣,這兩個(gè)方法的作用很相似蝶溶,唯一的區(qū)別就是他們執(zhí)行人物的間隔時(shí)間的計(jì)算方式,前者時(shí)間間隔算法是根據(jù)指定的 period 時(shí)間和任務(wù)執(zhí)行時(shí)間中取時(shí)間長(zhǎng)的宣渗,后者取的是指定的 delay 時(shí)間 + 任務(wù)執(zhí)行時(shí)間抖所。如果同學(xué)們有興趣,可以將上面的代碼跑跑看痕囱。一樣便能看出端倪田轧。
好了,JDK 給我們封裝了創(chuàng)建線程池的 4 個(gè)方法鞍恢,但是涯鲁,請(qǐng)注意,由于這些方法高度封裝有序,因此抹腿,如果使用不當(dāng),出了問題將無從排查旭寿,因此警绩,我建議,程序員應(yīng)到自己手動(dòng)創(chuàng)建線程池盅称,而手動(dòng)創(chuàng)建的前提就是高度了解線程池的參數(shù)設(shè)置肩祥。那么我們就來看看如何手動(dòng)創(chuàng)建線程池后室。
2. 如何手動(dòng)創(chuàng)建線程池
下面是一個(gè)手動(dòng)創(chuàng)建線程池的范本:
/**
* 默認(rèn)5條線程(默認(rèn)數(shù)量,即最少數(shù)量)混狠,
* 最大20線程(指定了線程池中的最大線程數(shù)量)岸霹,
* 空閑時(shí)間0秒(當(dāng)線程池梳理超過核心數(shù)量時(shí),多余的空閑時(shí)間的存活時(shí)間将饺,即超過核心線程數(shù)量的空閑線程贡避,在多長(zhǎng)時(shí)間內(nèi),會(huì)被銷毀)予弧,
* 等待隊(duì)列長(zhǎng)度1024刮吧,
* 線程名稱[MXR-Task-%d],方便回溯,
* 拒絕策略:當(dāng)任務(wù)隊(duì)列已滿掖蛤,拋出RejectedExecutionException
* 異常杀捻。
*/
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 20, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024)
, new ThreadFactoryBuilder().setNameFormat("My-Task-%d").build()
, new AbortPolicy()
);
我們看到,ThreadPoolExecutor 也就是線程池有 7 個(gè)參數(shù)蚓庭,我們一起來好好看看:
- corePoolSize 線程池中核心線程數(shù)量
- maximumPoolSize 最大線程數(shù)量
- keepAliveTime 空閑時(shí)間(當(dāng)線程池梳理超過核心數(shù)量時(shí)致讥,多余的空閑時(shí)間的存活時(shí)間,即超過核心線程數(shù)量的空閑線程器赞,在多長(zhǎng)時(shí)間內(nèi)垢袱,會(huì)被銷毀)
- unit 時(shí)間單位
- workQueue 當(dāng)核心線程工作已滿,需要存儲(chǔ)任務(wù)的隊(duì)列
- threadFactory 創(chuàng)建線程的工廠
- handler 當(dāng)隊(duì)列滿了之后的拒絕策略
前面幾個(gè)參數(shù)我們就不講了拳魁,很簡(jiǎn)單,主要是后面幾個(gè)參數(shù)撮弧,隊(duì)列潘懊,線程工廠,拒絕策略贿衍。
我們先看看隊(duì)列授舟,線程池默認(rèn)提供了 4 個(gè)隊(duì)列。
- 無界隊(duì)列: 默認(rèn)大小 int 最大值贸辈,因此可能會(huì)耗盡系統(tǒng)內(nèi)存释树,引起OOM,非常危險(xiǎn)擎淤。
- 直接提交的隊(duì)列 : 沒有容量奢啥,不會(huì)保存,直接創(chuàng)建新的線程嘴拢,因此需要設(shè)置很大的線程池?cái)?shù)桩盲。否則容易執(zhí)行拒絕策略,也很危險(xiǎn)席吴。
- 有界隊(duì)列:如果core滿了赌结,則存儲(chǔ)在隊(duì)列中捞蛋,如果core滿了且隊(duì)列滿了,則創(chuàng)建線程柬姚,直到maximumPoolSize 到了拟杉,如果隊(duì)列滿了且最大線程數(shù)已經(jīng)到了,則執(zhí)行拒絕策略量承。
- 優(yōu)先級(jí)隊(duì)列:按照優(yōu)先級(jí)執(zhí)行任務(wù)搬设。也可以設(shè)置大小。
樓主在自己的項(xiàng)目中使用了無界隊(duì)列宴合,但是設(shè)置了任務(wù)大小焕梅,1024。如果你的任務(wù)很多卦洽,建議分為多個(gè)線程池贞言。不要把雞蛋放在一個(gè)籃子里。
再看看拒絕策略阀蒂,什么是拒絕策略呢该窗?當(dāng)隊(duì)列滿了,如何處理那些仍然提交的任務(wù)蚤霞。JDK 默認(rèn)有4種策略酗失。
- AbortPolicy :直接拋出異常,阻止系統(tǒng)正常工作.
- CallerRunsPolicy : 只要線程池未關(guān)閉昧绣,該策略直接在調(diào)用者線程中规肴,運(yùn)行當(dāng)前被丟棄的任務(wù)。顯然這樣做不會(huì)真的丟棄任務(wù)夜畴,但是拖刃,任務(wù)提交線程的性能極有可能會(huì)急劇下降。
- DiscardOldestPolicy: 該策略將丟棄最老的一個(gè)請(qǐng)求贪绘,也就是即將被執(zhí)行的一個(gè)任務(wù)兑牡,并嘗試再次提交當(dāng)前任務(wù).
- DiscardPolicy: 該策略默默地丟棄無法處理的任務(wù),不予任何處理税灌,如果允許任務(wù)丟失均函,我覺得這是最好的方案.
當(dāng)然,如果你不滿意JDK提供的拒絕策略菱涤,可以自己實(shí)現(xiàn)苞也,只需要實(shí)現(xiàn) RejectedExecutionHandler 接口,并重寫 rejectedExecution 方法即可粘秆。
最后墩朦,線程工廠,線程池的所有線程都由線程工廠來創(chuàng)建翻擒,而默認(rèn)的線程工廠太過單一氓涣,我們看看默認(rèn)的線程工廠是如何創(chuàng)建線程的:
/**
* The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
可以看到牛哺,線程名稱為 pool- + 線程池編號(hào) + -thread- + 線程編號(hào) 。設(shè)置為非守護(hù)線程劳吠。優(yōu)先級(jí)為默認(rèn)引润。
如果我們想修改名稱呢?對(duì)痒玩,實(shí)現(xiàn) ThreadFactory 接口淳附,重寫 newThread 方法即可。但是已經(jīng)有人造好輪子了蠢古, 比如我們的例子中使用的 google 的 guaua 提供的 ThreadFactoryBuilder 工廠奴曙。可以自定義線程名稱草讶,是否守護(hù)洽糟,優(yōu)先級(jí),異常處理等等堕战,功能強(qiáng)大坤溃。
3. 如何擴(kuò)展線程池
那么我們能擴(kuò)展線程池的功能嗎?比如記錄線程任務(wù)的執(zhí)行時(shí)間嘱丢。實(shí)際上薪介,JDK 的線程池已經(jīng)為我們預(yù)留的接口,在線程池核心方法中越驻,有2 個(gè)方法是空的汁政,就是給我們預(yù)留的记劈。還有一個(gè)線程池退出時(shí)會(huì)調(diào)用的方法抠蚣。我們看看例子:
/**
* 如何擴(kuò)展線程池履澳,重寫 beforeExecute, afterExecute, terminated 方法距贷,這三個(gè)方法默認(rèn)是空的忠蝗。
*
* 可以監(jiān)控每個(gè)線程任務(wù)執(zhí)行的開始和結(jié)束時(shí)間阁最,或者自定義一些增強(qiáng)速种。
*
* 在 Worker 的 runWork 方法中配阵,會(huì)調(diào)用這些方法
*/
public class ExtendThreadPoolDemo {
static class MyTask implements Runnable {
String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out
.println("正在執(zhí)行:Thread ID:" + Thread.currentThread().getId() + ", Task Name = " + name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("準(zhǔn)備執(zhí)行:" + ((MyTask) r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("執(zhí)行完成: " + ((MyTask) r).name);
}
@Override
protected void terminated() {
System.out.println("線程池退出");
}
};
for (int i = 0; i < 5; i++) {
MyTask myTask = new MyTask("TASK-GEYM-" + i);
es.execute(myTask);
Thread.sleep(10);
}
es.shutdown();
}
}
我們重寫了 beforeExecute 方法救拉,也就是執(zhí)行任務(wù)之前會(huì)調(diào)用該方法亿絮,而 afterExecute 方法則是在任務(wù)執(zhí)行完畢后會(huì)調(diào)用該方法拂铡。還有一個(gè) terminated 方法斗锭,在線程池退出時(shí)會(huì)調(diào)用該方法岖是。執(zhí)行結(jié)果是什么呢实苞?
可以看到聪轿,每個(gè)任務(wù)執(zhí)行前后都會(huì)調(diào)用 before 和 after 方法猾浦。相當(dāng)于執(zhí)行了一個(gè)切面音瓷。而在調(diào)用 shutdown 方法后則會(huì)調(diào)用 terminated 方法绳慎。
4. 如何優(yōu)化線程池的異常信息
如何優(yōu)化線程池的異常信息杏愤? 在說這個(gè)問題之前珊楼,我們先說一個(gè)不容易發(fā)現(xiàn)的bug:
看代碼:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0L,
TimeUnit.MILLISECONDS, new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
executor.submit(new DivTask(100, i));
}
}
static class DivTask implements Runnable {
int a, b;
public DivTask(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
double re = a / b;
System.out.println(re);
}
}
執(zhí)行結(jié)果:
注意:只有4個(gè)結(jié)果邓了,其中一個(gè)結(jié)果被吞沒了骗炉,并且沒有任何信息蛇受。為什么呢?如果仔細(xì)看代碼乍丈,會(huì)發(fā)現(xiàn)轻专,在進(jìn)行 100 / 0 的時(shí)候肯定會(huì)報(bào)錯(cuò)的请垛,但是卻沒有報(bào)錯(cuò)信息,令人頭痛亚兄,為什么呢审胚?實(shí)際上颓影,如果你使用 execute 方法則會(huì)打印錯(cuò)誤信息懒鉴,當(dāng)你使用 submit 方法卻沒有調(diào)用它的get 方法璃俗,異常將會(huì)被吞沒城豁,因?yàn)槌牵绻l(fā)生了異常间聊,異常是作為返回值返回的哎榴。
怎么辦呢尚蝌?我們當(dāng)然可以使用 execute 方法飘言,但是我們可以有另一種方式:重寫 submit 方法热凹,樓主寫了一個(gè)例子般妙,大家看一下:
static class TraceThreadPoolExecutor extends ThreadPoolExecutor {
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
// super.execute(command);
super.execute(wrap(command, clientTrace(), Thread.currentThread().getName()));
}
@Override
public Future<?> submit(Runnable task) {
// return super.submit(task);
return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
private Exception clientTrace() {
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task, final Exception clientStack,
String clientThreaName) {
return new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
clientStack.printStackTrace();
throw e;
}
}
};
}
}
我們重寫了 submit 方法,封裝了異常信息苫拍,如果發(fā)生了異常绒极,將會(huì)打印堆棧信息榔袋。我們看看使用重寫后的線程池后的結(jié)果是什么铡俐?
從結(jié)果中吏够,我們清楚的看到了錯(cuò)誤信息的原因:by zero锅知!并且堆棧信息明確,方便排錯(cuò)侣姆。優(yōu)化了默認(rèn)線程池的策略。
5. 如何設(shè)計(jì)線程池中的線程數(shù)量
線程池的大小對(duì)系統(tǒng)的性能有一定的影響沉噩,過大或者過小的線程數(shù)量都無法發(fā)揮最優(yōu)的系統(tǒng)性能捺宗,但是線程池大小的確定也不需要做的非常精確。因?yàn)橹灰苊鈽O大和極小兩種情況川蒙,線程池的大小對(duì)性能的影響都不會(huì)影響太大蚜厉,一般來說,確定線程池的大小需要考慮CPU數(shù)量畜眨,內(nèi)存大小等因素昼牛,在《Java Concurrency in Practice》 書中給出了一個(gè)估算線程池大小的經(jīng)驗(yàn)公式:
公式還是有點(diǎn)復(fù)雜的,簡(jiǎn)單來說康聂,就是如果你是CPU密集型運(yùn)算贰健,那么線程數(shù)量和CPU核心數(shù)相同就好氓侧,避免了大量無用的切換線程上下文独郎,如果你是IO密集型的話,需要大量等待,那么線程數(shù)可以設(shè)置的多一些,比如CPU核心乘以2.
至于如何獲取 CPU 核心數(shù),Java 提供了一個(gè)方法:
Runtime.getRuntime().availableProcessors();
返回了CPU的核心數(shù)量。
總結(jié)
好了拍皮,到這里爹橱,我們已經(jīng)對(duì)如何使用線程池有了一個(gè)認(rèn)識(shí)冯键,這里,樓主建議大家手動(dòng)創(chuàng)建線程池枉昏,這樣對(duì)線程池中的各個(gè)參數(shù)可以有精準(zhǔn)的了解晰奖,在對(duì)系統(tǒng)進(jìn)行排錯(cuò)或者調(diào)優(yōu)的時(shí)候有好處蛆楞。比如設(shè)置核心線程數(shù)多少合適,最大線程數(shù),拒絕策略,線程工廠冀瓦,隊(duì)列的大小和類型等等尼啡,也可以是G家的線程工廠自定義線程书聚。
下一篇,我們將深入源碼,看看JDK 的線程池是如何實(shí)現(xiàn)的冲九。因此灭贷,先熟悉線程池的使用吧L友印;苹尽!
good luckJ鞘取@鍪痢坠非!