成都創(chuàng)新互聯(lián)主營(yíng)下陸網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,成都app軟件開(kāi)發(fā)公司,下陸h5微信小程序定制開(kāi)發(fā)搭建,下陸網(wǎng)站營(yíng)銷(xiāo)推廣歡迎下陸等地區(qū)企業(yè)咨詢
雖然在程序中可以直接使用Thread類(lèi)型來(lái)進(jìn)行線程操作,但是更多的情況是使用線程池,尤其是在Java EE應(yīng)用服務(wù)器中,一般會(huì)使用若干個(gè)線程池來(lái)處理來(lái)自客戶端的請(qǐng)求。Java中對(duì)于線程池的支持,來(lái)自ThreadPoolExecutor。一些應(yīng)用服務(wù)器也確實(shí)是使用的ThreadPoolExecutor來(lái)實(shí)現(xiàn)線程池。
對(duì)于線程池的性能調(diào)優(yōu),最重要的參數(shù)就是線程池的大小。
在ThreadPoolExecutor和其相關(guān)的類(lèi)型中,最小線程數(shù)被稱為線程池核心規(guī)模(Core Pool Size),在其它Java應(yīng)用服務(wù)器的實(shí)現(xiàn)中,這個(gè)數(shù)量也許被稱為最小線程數(shù)(MinThreads),但是它們的概念是相同的。
但是在對(duì)線程池進(jìn)行規(guī)模變更(Resizing)的時(shí)候,ThreadPoolExecutor和其它線程池的實(shí)現(xiàn)也許存在的很大的差別。
一個(gè)最簡(jiǎn)單的情況是:當(dāng)有新任務(wù)需要被執(zhí)行,且當(dāng)前所有的線程都被占用時(shí),ThreadPoolExecutor和其它實(shí)現(xiàn)通常都會(huì)新創(chuàng)建一個(gè)線程來(lái)執(zhí)行這個(gè)新任務(wù)(直到達(dá)到了最大線程數(shù))。
為了方便討論,下面假設(shè)JVM有4個(gè)可用的CPU。那么任務(wù)也很明確,就是要最大程度地“壓榨”它們的資源,千方百計(jì)的提高CPU的利用率。
那么,最大線程數(shù)最少需要被設(shè)置成4,因?yàn)橛?個(gè)可用的CPU,意味著最多能夠并行地執(zhí)行4個(gè)任務(wù)。當(dāng)然,垃圾回收(Garbage Collection)在這個(gè)過(guò)程中也會(huì)造成一些影響,但是它們往往不需要使用整個(gè)CPU。一個(gè)例外是,當(dāng)使用了CMS或者G1垃圾回收算法時(shí),需要有足夠的CPU資源進(jìn)行垃圾回收。
那么是否有必要將線程數(shù)量設(shè)置的更大呢?這就取決于任務(wù)的特征了。
假設(shè)當(dāng)任務(wù)是計(jì)算密集型的,意味著任務(wù)不需要執(zhí)行IO操作,例如讀取數(shù)據(jù)庫(kù),讀取文件等,因此它們不涉及到同步的問(wèn)題,任務(wù)之間完全是獨(dú)立的。比如使用一個(gè)批處理程序讀取Mock數(shù)據(jù)源的數(shù)據(jù),測(cè)試在不線程池?fù)碛胁煌€程數(shù)量時(shí)的性能,得到下表:
當(dāng)計(jì)算是通過(guò)Servlet觸發(fā)的時(shí)候,性能數(shù)據(jù)是下面這個(gè)樣子的(Load Generator會(huì)同時(shí)發(fā)送20個(gè)請(qǐng)求):
下面,從Client的角度考慮一下問(wèn)題,并發(fā)Client的數(shù)量對(duì)于Server的響應(yīng)時(shí)間會(huì)有什么影響呢?還是同樣地環(huán)境,當(dāng)并發(fā)Client數(shù)量逐漸增加時(shí),響應(yīng)時(shí)間會(huì)如下發(fā)生變化:
因?yàn)槿蝿?wù)類(lèi)型是計(jì)算密集型的,當(dāng)并發(fā)Client數(shù)量時(shí)1,2,4時(shí),平均響應(yīng)時(shí)間都是最優(yōu)的,然而當(dāng)出現(xiàn)多余4個(gè)Client時(shí),性能會(huì)隨著Client的增加發(fā)生顯著地下降。
當(dāng)Client數(shù)量增加時(shí),你也許會(huì)想通過(guò)增加服務(wù)端線程池的線程數(shù)量來(lái)提高性能,可是在CPU密集型任務(wù)的情況下,這么做只會(huì)降低性能。因?yàn)橄到y(tǒng)的瓶頸就是CPU資源,冒然增加線程池的線程數(shù)量只會(huì)讓對(duì)于這種資源的競(jìng)爭(zhēng)更加激烈。
所以,在面對(duì)性能方面的問(wèn)題時(shí)。第一步永遠(yuǎn)是了解系統(tǒng)的瓶頸在哪里,這樣才能夠有的放矢。如果冒然進(jìn)行所謂的“調(diào)優(yōu)”,讓對(duì)瓶頸資源的競(jìng)爭(zhēng)更加激烈,那么帶來(lái)的只會(huì)是性能的進(jìn)一步下降。相反,如果讓對(duì)瓶頸資源的競(jìng)爭(zhēng)變的緩和,那么性能通常則會(huì)提高。
在上面的場(chǎng)景中,如果從ThreadPoolExecutor的角度進(jìn)行考慮,那么在任務(wù)隊(duì)列中一直會(huì)有任務(wù)處于掛起(Pending)的狀態(tài)(因?yàn)镃lient的每個(gè)請(qǐng)求對(duì)應(yīng)的就是一個(gè)任務(wù)),而所有的可用線程都在工作,CPU正在滿負(fù)荷運(yùn)轉(zhuǎn)。這個(gè)時(shí)候添加線程池的線程數(shù)量,讓這些添加的線程領(lǐng)取一些掛起的任務(wù),會(huì)發(fā)生什么事情呢?這時(shí)帶來(lái)的只會(huì)是線程之間對(duì)于CPU資源的爭(zhēng)奪更加激烈,降低了性能。
設(shè)置了最大線程數(shù)之后,還需要設(shè)置最小線程數(shù)。對(duì)于絕大部分場(chǎng)景,將它設(shè)置的和最大線程數(shù)相等就可以了。
將最小線程數(shù)設(shè)置的小于最大線程數(shù)的初衷是為了節(jié)省資源,因?yàn)槊慷鄤?chuàng)建一個(gè)線程都會(huì)耗費(fèi)一定量的資源,尤其是線程棧所需要的資源。但是在一個(gè)系統(tǒng)中,針對(duì)硬件資源以及任務(wù)特點(diǎn)選定了最大線程數(shù)之后,就表示這個(gè)系統(tǒng)總是會(huì)利用這些線程的,那么還不如在一開(kāi)始就讓線程池把需要的線程準(zhǔn)備好。然而,把最小線程數(shù)設(shè)置的小于最大線程數(shù)所帶來(lái)的影響也是非常小的,一般都不會(huì)察覺(jué)到有什么不同。
在批處理程序中,最小線程數(shù)是否等于最大線程數(shù)并不重要。因?yàn)樽詈缶€程總是需要被創(chuàng)建出來(lái)的,所以程序的運(yùn)行時(shí)間應(yīng)該幾乎相同。對(duì)于服務(wù)器程序而言,影響也不大,但是一般而言,線程池中的線程在“熱身”階段就應(yīng)該被創(chuàng)建出來(lái),所以這也是為什么建議將最小線程數(shù)設(shè)置的等于最大線程數(shù)的原因。
在一些場(chǎng)景中,也需要要設(shè)置一個(gè)不同的最小線程數(shù)。比如當(dāng)一個(gè)系統(tǒng)最大需要同時(shí)處理2000個(gè)任務(wù),而平均任務(wù)數(shù)量只是20個(gè)情況下,就需要將最小線程數(shù)設(shè)置成20,而不是等于其最大線程數(shù)2000。此時(shí)如果還是將最小線程數(shù)設(shè)置的等于最大線程數(shù)的話,那么閑置線程(Idle Thread)占用的資源就比較可觀了,尤其是當(dāng)使用了ThreadLocal類(lèi)型的變量時(shí)。
線程池有一個(gè)列表或者隊(duì)列的數(shù)據(jù)結(jié)構(gòu)來(lái)存放需要被執(zhí)行的任務(wù)。顯然,在某些情況下,任務(wù)數(shù)量的增長(zhǎng)速度會(huì)大于其被執(zhí)行的速度。如果這個(gè)任務(wù)代表的是一個(gè)來(lái)自Client的請(qǐng)求,那么也就意味著該Client會(huì)等待比較長(zhǎng)的時(shí)間。顯然這是不可接受的,尤其對(duì)于提供Web服務(wù)的服務(wù)器程序而言。
所以,線程池會(huì)有機(jī)制來(lái)限制列表/隊(duì)列中任務(wù)的數(shù)量。但是,和設(shè)置最大線程數(shù)一樣,并沒(méi)有一個(gè)放之四海而皆準(zhǔn)的最優(yōu)任務(wù)數(shù)量。這還是要取決于具體的任務(wù)類(lèi)型和不斷的進(jìn)行性能測(cè)試。
對(duì)于ThreadPoolExecutor而言,當(dāng)任務(wù)數(shù)量達(dá)到最大時(shí),再嘗試增加新的任務(wù)就會(huì)失敗。ThreadPoolExecutor有一個(gè)rejectedExecution方法用來(lái)拒絕該任務(wù)。這會(huì)導(dǎo)致應(yīng)用服務(wù)器返回一個(gè)HTTP狀態(tài)碼500,當(dāng)然這種信息最好以更友好的方式傳達(dá)給Client,比如解釋一下為什么你的請(qǐng)求被拒絕了。
關(guān)于如何定制ThreadPoolExecutor,遵循KISS原則(Keep It Simple, Stupid)就好了。比如將最大線程數(shù)和最小線程數(shù)設(shè)置的相等,然后根據(jù)情況選擇有限隊(duì)列或者無(wú)限隊(duì)列。
線程池是對(duì)象池的一個(gè)有用的例子,它能夠節(jié)省在創(chuàng)建它們時(shí)候的資源開(kāi)銷(xiāo)。并且線程池對(duì)系統(tǒng)中的線程數(shù)量也起到了很好的限制作用。
線程池中的線程數(shù)量必須仔細(xì)的設(shè)置,否則冒然增加線程數(shù)量只會(huì)帶來(lái)性能的下降。
在Java 7中引入了一種新的線程池:ForkJoinPool。
它同ThreadPoolExecutor一樣,也實(shí)現(xiàn)了Executor和ExecutorService接口。它使用了一個(gè)無(wú)限隊(duì)列來(lái)保存需要執(zhí)行的任務(wù),而線程的數(shù)量則是通過(guò)構(gòu)造函數(shù)傳入,如果沒(méi)有向構(gòu)造函數(shù)中傳入希望的線程數(shù)量,那么當(dāng)前計(jì)算機(jī)可用的CPU數(shù)量會(huì)被設(shè)置為線程數(shù)量作為默認(rèn)值。
ForkJoinPool主要用來(lái)使用分治法(Divide-and-Conquer Algorithm)來(lái)解決問(wèn)題。典型的應(yīng)用比如快速排序算法。這里的要點(diǎn)在于,F(xiàn)orkJoinPool需要使用相對(duì)少的線程來(lái)處理大量的任務(wù)。比如要對(duì)1000萬(wàn)個(gè)數(shù)據(jù)進(jìn)行排序,那么會(huì)將這個(gè)任務(wù)分割成兩個(gè)500萬(wàn)的排序任務(wù)和一個(gè)針對(duì)這兩組500萬(wàn)數(shù)據(jù)的合并任務(wù)。以此類(lèi)推,對(duì)于500萬(wàn)的數(shù)據(jù)也會(huì)做出同樣的分割處理,到最后會(huì)設(shè)置一個(gè)閾值來(lái)規(guī)定當(dāng)數(shù)據(jù)規(guī)模到多少時(shí),停止這樣的分割處理。比如,當(dāng)元素的數(shù)量小于10時(shí),會(huì)停止分割,轉(zhuǎn)而使用插入排序?qū)λ鼈冞M(jìn)行排序。
那么到最后,所有的任務(wù)加起來(lái)會(huì)有大概2000000+個(gè)。問(wèn)題的關(guān)鍵在于,對(duì)于一個(gè)任務(wù)而言,只有當(dāng)它所有的子任務(wù)完成之后,它才能夠被執(zhí)行。
所以當(dāng)使用ThreadPoolExecutor時(shí),使用分治法會(huì)存在問(wèn)題,因?yàn)門(mén)hreadPoolExecutor中的線程無(wú)法像任務(wù)隊(duì)列中再添加一個(gè)任務(wù)并且在等待該任務(wù)完成之后再繼續(xù)執(zhí)行。而使用ForkJoinPool時(shí),就能夠讓其中的線程創(chuàng)建新的任務(wù),并掛起當(dāng)前的任務(wù),此時(shí)線程就能夠從隊(duì)列中選擇子任務(wù)執(zhí)行。
比如,我們需要統(tǒng)計(jì)一個(gè)double數(shù)組中小于0.5的元素的個(gè)數(shù),那么可以使用ForkJoinPool進(jìn)行實(shí)現(xiàn)如下:
public class ForkJoinTest {
private double[] d;
private class ForkJoinTask extends RecursiveTask {
private int first;
private int last;
public ForkJoinTask(int first, int last) {
this.first = first;
this.last = last;
}
protected Integer compute() {
int subCount;
if (last - first < 10) {
subCount = 0;
for (int i = first; i <= last; i++) {
if (d[i] < 0.5)
subCount++;
}
} else {
int mid = (first + last) >>> 1;
ForkJoinTask left = new ForkJoinTask(first, mid);
left.fork();
ForkJoinTask right = new ForkJoinTask(mid + 1, last);
right.fork();
subCount = left.join();
subCount += right.join();
}
return subCount;
}
}
public static void main(String[] args) {
d = createArrayOfRandomDoubles();
int n = new ForkJoinPool().invoke(new ForkJoinTask(0, 9999999));
System.out.println("Found " + n + " values");
}
}
以上的關(guān)鍵是fork()和join()方法。在ForkJoinPool使用的線程中,會(huì)使用一個(gè)內(nèi)部隊(duì)列來(lái)對(duì)需要執(zhí)行的任務(wù)以及子任務(wù)進(jìn)行操作來(lái)保證它們的執(zhí)行順序。
那么使用ThreadPoolExecutor或者ForkJoinPool,會(huì)有什么性能的差異呢?
首先,使用ForkJoinPool能夠使用數(shù)量有限的線程來(lái)完成非常多的具有父子關(guān)系的任務(wù),比如使用4個(gè)線程來(lái)完成超過(guò)200萬(wàn)個(gè)任務(wù)。但是,使用ThreadPoolExecutor時(shí),是不可能完成的,因?yàn)門(mén)hreadPoolExecutor中的Thread無(wú)法選擇優(yōu)先執(zhí)行子任務(wù),需要完成200萬(wàn)個(gè)具有父子關(guān)系的任務(wù)時(shí),也需要200萬(wàn)個(gè)線程,顯然這是不可行的。
當(dāng)然,在上面的例子中,也可以不使用分治法,因?yàn)槿蝿?wù)之間的獨(dú)立性,可以將整個(gè)數(shù)組劃分為幾個(gè)區(qū)域,然后使用ThreadPoolExecutor來(lái)解決,這種辦法不會(huì)創(chuàng)建數(shù)量龐大的子任務(wù)。代碼如下:
public class ThreadPoolTest {
private double[] d;
private class ThreadPoolExecutorTask implements Callable {
private int first;
private int last;
public ThreadPoolExecutorTask(int first, int last) {
this.first = first;
this.last = last;
}
public Integer call() {
int subCount = 0;
for (int i = first; i <= last; i++) {
if (d[i] < 0.5) {
subCount++;
}
}
return subCount;
}
}
public static void main(String[] args) {
d = createArrayOfRandomDoubles();
ThreadPoolExecutor tpe = new ThreadPoolExecutor
(4, 4, long.MAX_VALUE, TimeUnit.SECONDS, new LinkedBlockingQueue());
Future[] f = new Future[4];
int size = d.length / 4;
for (int i = 0; i < 3; i++) {
f[i] = tpe.submit(new ThreadPoolExecutorTask(i * size, (i + 1) * size - 1);
}
f[3] = tpe.submit(new ThreadPoolExecutorTask(3 * size, d.length - 1);
int n = 0;
for (int i = 0; i < 4; i++) {
n += f.get();
}
System.out.println("Found " + n + " values");
}
}
在分別使用ForkJoinPool和ThreadPoolExecutor時(shí),它們處理這個(gè)問(wèn)題的時(shí)間如下:
對(duì)執(zhí)行過(guò)程中的GC同樣也進(jìn)行了監(jiān)控,發(fā)現(xiàn)在使用ForkJoinPool時(shí),總的GC時(shí)間花去了1.2s,而ThreadPoolExecutor并沒(méi)有觸發(fā)任何的GC操作。這是因?yàn)樵贔orkJoinPool的運(yùn)行過(guò)程中,會(huì)創(chuàng)建大量的子任務(wù)。而當(dāng)他們執(zhí)行完畢之后,會(huì)被垃圾回收。反之,ThreadPoolExecutor則不會(huì)創(chuàng)建任何的子任務(wù),因此不會(huì)導(dǎo)致任何的GC操作。
ForkJoinPool的另外一個(gè)特性是它能夠?qū)崿F(xiàn)工作竊取(Work Stealing),在該線程池的每個(gè)線程中會(huì)維護(hù)一個(gè)隊(duì)列來(lái)存放需要被執(zhí)行的任務(wù)。當(dāng)線程自身隊(duì)列中的任務(wù)都執(zhí)行完畢后,它會(huì)從別的線程中拿到未被執(zhí)行的任務(wù)并幫助它執(zhí)行。
可以通過(guò)以下的代碼來(lái)測(cè)試ForkJoinPool的Work Stealing特性:
for (int i = first; i <= last; i++) {
if (d[i] < 0.5) {
subCount++;
}
for (int j = 0; j < d.length - i; j++) {
for (int k = 0; k < 100; k++) {
dummy = j * k + i;
// dummy is volatile, so multiple writes occur
d[i] = dummy;
}
}
}
因?yàn)槔飳拥难h(huán)次數(shù)(j)是依賴于外層的i的值的,所以這段代碼的執(zhí)行時(shí)間依賴于i的值。當(dāng)i = 0時(shí),執(zhí)行時(shí)間最長(zhǎng),而i = last時(shí)執(zhí)行時(shí)間最短。也就意味著任務(wù)的工作量是不一樣的,當(dāng)i的值較小時(shí),任務(wù)的工作量大,隨著i逐漸增加,任務(wù)的工作量變小。因此這是一個(gè)典型的任務(wù)負(fù)載不均衡的場(chǎng)景。
這時(shí),選擇ThreadPoolExecutor就不合適了,因?yàn)樗渲械木€程并不會(huì)關(guān)注每個(gè)任務(wù)之間任務(wù)量的差異。當(dāng)執(zhí)行任務(wù)量最小的任務(wù)的線程執(zhí)行完畢后,它就會(huì)處于空閑的狀態(tài)(Idle),等待任務(wù)量最大的任務(wù)執(zhí)行完畢。
而ForkJoinPool的情況就不同了,即使任務(wù)的工作量有差別,當(dāng)某個(gè)線程在執(zhí)行工作量大的任務(wù)時(shí),其他的空閑線程會(huì)幫助它完成剩下的任務(wù)。因此,提高了線程的利用率,從而提高了整體性能。
這兩種線程池對(duì)于任務(wù)工作量不均衡時(shí)的執(zhí)行時(shí)間:
注意到當(dāng)線程數(shù)量為1時(shí),兩者的執(zhí)行時(shí)間差異并不明顯。這是因?yàn)榭偟挠?jì)算量是相同的,而ForkJoinPool慢的那一秒多是因?yàn)樗鼊?chuàng)建了非常多的任務(wù),同時(shí)也導(dǎo)致了GC的工作量增加。
當(dāng)線程數(shù)量增加到4時(shí),執(zhí)行時(shí)間的區(qū)別就較大了,F(xiàn)orkJoinPool的性能比ThreadPoolExecutor好將近50%,可見(jiàn)Work Stealing在應(yīng)對(duì)任務(wù)量不均衡的情況下,能夠保證資源的利用率。
所以一個(gè)結(jié)論就是:當(dāng)任務(wù)的任務(wù)量均衡時(shí),選擇ThreadPoolExecutor往往更好,反之則選擇ForkJoinPool。
另外,對(duì)于ForkJoinPool,還有一個(gè)因素會(huì)影響它的性能,就是停止進(jìn)行任務(wù)分割的那個(gè)閾值。比如在之前的快速排序中,當(dāng)剩下的元素?cái)?shù)量小于10的時(shí)候,就會(huì)停止子任務(wù)的創(chuàng)建。下表顯示了在不同閾值下,F(xiàn)orkJoinPool的性能:
可以發(fā)現(xiàn),當(dāng)閾值不同時(shí),對(duì)于性能也會(huì)有一定影響。因此,在使用ForkJoinPool時(shí),對(duì)此閾值進(jìn)行測(cè)試,使用一個(gè)最合適的值也有助于整體性能。
在Java 8中,引入了自動(dòng)并行化的概念。它能夠讓一部分Java代碼自動(dòng)地以并行的方式執(zhí)行,前提是使用了ForkJoinPool。
Java 8為ForkJoinPool添加了一個(gè)通用線程池,這個(gè)線程池用來(lái)處理那些沒(méi)有被顯式提交到任何線程池的任務(wù)。它是ForkJoinPool類(lèi)型上的一個(gè)靜態(tài)元素,它擁有的默認(rèn)線程數(shù)量等于運(yùn)行計(jì)算機(jī)上的處理器數(shù)量。
當(dāng)調(diào)用Arrays類(lèi)上添加的新方法時(shí),自動(dòng)并行化就會(huì)發(fā)生。比如用來(lái)排序一個(gè)數(shù)組的并行快速排序,用來(lái)對(duì)一個(gè)數(shù)組中的元素進(jìn)行并行遍歷。自動(dòng)并行化也被運(yùn)用在Java 8新添加的Stream API中。
比如下面的代碼用來(lái)遍歷列表中的元素并執(zhí)行需要的計(jì)算:
Stream stream = arrayList.parallelStream();
stream.forEach(a -> {
String symbol = StockPriceUtils.makeSymbol(a);
StockPriceHistory sph = new StockPriceHistoryImpl(symbol, startDate, endDate, entityManager);
}
);
對(duì)于列表中的元素的計(jì)算都會(huì)以并行的方式執(zhí)行。forEach方法會(huì)為每個(gè)元素的計(jì)算操作創(chuàng)建一個(gè)任務(wù),該任務(wù)會(huì)被前文中提到的ForkJoinPool中的通用線程池處理。以上的并行計(jì)算邏輯當(dāng)然也可以使用ThreadPoolExecutor完成,但是就代碼的可讀性和代碼量而言,使用ForkJoinPool明顯更勝一籌。
對(duì)于ForkJoinPool通用線程池的線程數(shù)量,通常使用默認(rèn)值就可以了,即運(yùn)行時(shí)計(jì)算機(jī)的處理器數(shù)量。如果需要調(diào)整線程數(shù)量,可以通過(guò)設(shè)置系統(tǒng)屬性:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
下面的一組數(shù)據(jù)用來(lái)比較使用ThreadPoolExecutor和ForkJoinPool中的通用線程池來(lái)完成上面簡(jiǎn)單計(jì)算時(shí)的性能:
注意到當(dāng)線程數(shù)為1,2,4時(shí),性能差異的比較明顯。線程數(shù)為1的ForkJoinPool通用線程池和線程數(shù)為2的ThreadPoolExecutor的性能十分接近。
出現(xiàn)這種現(xiàn)象的原因是,forEach方法用了一些小把戲。它會(huì)將執(zhí)行forEach本身的線程也作為線程池中的一個(gè)工作線程。因此,即使將ForkJoinPool的通用線程池的線程數(shù)量設(shè)置為1,實(shí)際上也會(huì)有2個(gè)工作線程。因此在使用forEach的時(shí)候,線程數(shù)為1的ForkJoinPool通用線程池和線程數(shù)為2的ThreadPoolExecutor是等價(jià)的。
所以當(dāng)ForkJoinPool通用線程池實(shí)際需要4個(gè)工作線程時(shí),可以將它設(shè)置成3,那么在運(yùn)行時(shí)可用的工作線程就是4了。
————END————