一、背景
我問(wèn)群里的同學(xué)奔穿,下一期的話題討論什么镜沽?一同學(xué)說(shuō)多線程,當(dāng)時(shí)聽(tīng)到這個(gè)話題本想拒絕的贱田,因?yàn)檫@個(gè)話題太泛了缅茉,很難寫(xiě)出一篇好的文章;涉及到的內(nèi)容又很多男摧,隨便拿出一塊就能討論幾天蔬墩;但為了大家的積級(jí)性,還是硬著頭皮應(yīng)下來(lái)(實(shí)際上我對(duì)這一塊也不怎么理解)耗拓,基本上花了一周的時(shí)候看了幾百頁(yè)的書(shū)拇颅,然后希望能夠有條理地寫(xiě)出一篇文章,但發(fā)現(xiàn)還是太難了乔询;如果包含太多的技術(shù)細(xì)節(jié)樟插,我甚至沒(méi)辦法把概念講清楚;后來(lái)我決定哥谷,寫(xiě)這篇文章為了大家理解JAVA多線程最基本的概念岸夯,涉及到的技術(shù)細(xì)節(jié)我們以子話題的形式,慢慢為大家更新们妥。
二猜扮、幾個(gè)基本概念
并發(fā)是指讓多個(gè)任務(wù)并行執(zhí)行(同時(shí)執(zhí)行多個(gè)任務(wù)),以使任務(wù)得到更快的執(zhí)行监婶。實(shí)現(xiàn)并發(fā)的方式旅赢,有兩個(gè):進(jìn)程機(jī)制和線程機(jī)制;一個(gè)CPU同時(shí)只能執(zhí)行一個(gè)進(jìn)程惑惶,
并發(fā)與并行
并發(fā)指的是具有處理多任務(wù)的能力
并行指的是具有同時(shí)處理多任務(wù)的能力
“并發(fā)”和“并行”唯一的一個(gè)區(qū)別在于“同時(shí)”煮盼,也從上可以看出,“并行”系統(tǒng)一定是“并發(fā)”系統(tǒng)带污,“并行”是“并發(fā)”的一個(gè)子集僵控。
下面來(lái)自知乎一個(gè)網(wǎng)友的回答用一個(gè)很好的場(chǎng)景解釋了它們的區(qū)別:
- 你吃飯吃到一半,電話來(lái)了鱼冀,你一直到吃完了以后才去接报破,這就說(shuō)明你不支持并發(fā)也不支持并行悠就。
- 你吃飯吃到一半,電話來(lái)了充易,你停了下來(lái)接了電話梗脾,接完后繼續(xù)吃飯,這說(shuō)明你支持并發(fā)盹靴。
- 你吃飯吃到一半炸茧,電話來(lái)了,你一邊打電話一邊吃飯稿静,這說(shuō)明你支持并行梭冠。
進(jìn)程與線程
進(jìn)程是資源分配的最小單位,同時(shí)也支持調(diào)度自赔;什么是資源呢妈嘹,就是內(nèi)存/io/文件等,進(jìn)程的資源是在開(kāi)啟的時(shí)候就分配好的绍妨,各進(jìn)程之間不可相互換資源。
線程是獨(dú)立運(yùn)行和獨(dú)立調(diào)度的基本單位柬脸。獨(dú)立運(yùn)行的意思是他去,線程才是真正的任傷執(zhí)行者,一個(gè)線程可以執(zhí)行多個(gè)任務(wù)(任務(wù)切換)倒堕。獨(dú)立調(diào)度即時(shí)間切片灾测,每個(gè)線程按照時(shí)間切片執(zhí)行任務(wù)。
線程依賴(lài)于進(jìn)程垦巴,進(jìn)程已經(jīng)申請(qǐng)了線程執(zhí)行任務(wù)時(shí)的資源媳搪,多個(gè)線程之間共享這部分資源,一個(gè)進(jìn)程可以有多個(gè)線程骤宣。
線程驅(qū)動(dòng)任務(wù)的執(zhí)行秦爆。
進(jìn)程與線程之間的關(guān)系看到一篇很好的文章,大家可以參考憔披。
一個(gè)小插曲:線程一定要依附于進(jìn)程嗎?(不一定等限,但在JAVA一定是),其實(shí)進(jìn)程和線程是實(shí)現(xiàn)并發(fā)的兩種方式,曾經(jīng)有人提出芬膝,并發(fā)的實(shí)現(xiàn)只應(yīng)該采用進(jìn)程的方式望门,因?yàn)榫€程的上下文切換存在不小的開(kāi)銷(xiāo),并不一定能夠提高任務(wù)的執(zhí)行效率锰霜。比如筹误,full GC,如果采用并行收集癣缅,其線程數(shù)量也不應(yīng)該超過(guò)計(jì)算機(jī)的核數(shù)厨剪,如果超過(guò)核數(shù)哄酝,其性能還不如順序收集。但為什么線程又能大行其道呢丽惶?這脫離不了任務(wù)執(zhí)行中的一個(gè)狀態(tài):“阻塞”炫七,當(dāng)其中一個(gè)任務(wù)處于阻塞狀態(tài)(比如等待輸入)時(shí),就可以執(zhí)行另外一個(gè)任務(wù)钾唬,這樣大大提高了執(zhí)行的效率万哪。線程打破了摩爾定律,又有人提出了阿姆達(dá)爾定律
JAVA采用的是多線程的編程機(jī)制抡秆,因此本文討論的主要問(wèn)題集中在奕巍,線程如何調(diào)度任務(wù),如何管理線程以及各線程之間的資源競(jìng)爭(zhēng)問(wèn)題
三儒士、任務(wù)
從上一節(jié)的描述中我們知道的止,最終執(zhí)行的是任務(wù),那我們就需要了解一下如何創(chuàng)建任務(wù)
3.1 創(chuàng)建任務(wù)
有兩種類(lèi)型的任務(wù)着撩,一種是不需要返回結(jié)果的runnable,另外一種是希望返回結(jié)果的Callable诅福。接下來(lái)我們就詳情了解一下,如何創(chuàng)建兩種任務(wù):
Runnable
先來(lái)看個(gè)例子(就一個(gè)遞減操作):
public class ListOff implements Runnable {
protected int countDown = 10;
public ListOff() {
}
public String status() {
return "#" + "(" + (countDown > 0 ? countDown : "Liftoff!") + ").";
}
public void run() {
while (countDown-- > 0) {
System.out.print(status());
}
System.out.println();
}
}
然后把這個(gè)任務(wù)注冊(cè)到多個(gè)線程上拖叙,并啟動(dòng)線程氓润,只需要new Thread再start就可以了,是不是很簡(jiǎn)單薯鳍。
public class TestThread {
@Test
public void testOneThread() {
Thread t = new Thread(new ListOff());
t.start();
}
@Test
public void testNThread() {
int i = 10;
while(i-- > 0) {
Thread t = new Thread(new ListOff());
t.start();
}
}
}
Callable
public class GetRand implements Callable<Integer>{
public Integer call() {
Random random = new Random();
return random.nextInt(100);
}
}
public class TestThread {
@Test
public void testCallable() throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<Integer>> results = new ArrayList<>();
int i = 10;
while (i-- > 0) {
results.add(exec.submit(new GetRand()));
}
for (Future<Integer> n : results) {
System.out.println(n.get());
}
}
}.
為什么要調(diào)用start?通過(guò)此代碼找到Thread中的start()方法的定義咖气,可以發(fā)現(xiàn)此方法中使用了private native void start0();其中native關(guān)鍵字表示可以調(diào)用操作系統(tǒng)的底層函數(shù),那么這樣的技術(shù)成為JNI技術(shù)(java Native Interface)
還有一種調(diào)度runnable的方法挖滤,那就是繼承Thread.
public class PThread extends Thread{
public PThread() {
super();
}
@Override
public void run () {
System.out.println(DateFormat.getDateInstance(DateFormat.DEFAULT).format(new Date()));
}
@Test
public void TestMain() {
int i = 10;
while(i-- > 0) {
new PThread().run();
}
}
}
兩種方法有什么區(qū)別呢(copy別人的結(jié)論)崩溪?
Java中實(shí)現(xiàn)多線程有兩種方法:繼承Thread類(lèi)、實(shí)現(xiàn)Runnable接口斩松,在程序開(kāi)發(fā)中只要是多線程伶唯,肯定永遠(yuǎn)以實(shí)現(xiàn)Runnable接口為主,因?yàn)閷?shí)現(xiàn)Runnable接口相比繼承Thread類(lèi)有如下優(yōu)勢(shì):
- 1砸民、可以避免由于Java的單繼承特性而帶來(lái)的局限抵怎;
- 2、增強(qiáng)程序的健壯性岭参,代碼能夠被多個(gè)線程共享反惕,代碼與數(shù)據(jù)是獨(dú)立的;
- 3演侯、適合多個(gè)相同程序代碼的線程區(qū)處理同一資源的情況姿染。
想要更加詳情地了解,參考
四 線程管理與調(diào)度
通過(guò)上面的任務(wù)介紹,我們可們簡(jiǎn)單了解了如何創(chuàng)建兩種類(lèi)型的任務(wù)悬赏。下面我們就具體來(lái)了解一下線程以及線程如何調(diào)度任務(wù)狡汉。
我們主要通過(guò)Executor框架來(lái)管理和調(diào)度線程
4.1 executor
通過(guò)new Thread()的方式去管理線程,在極簡(jiǎn)單的系統(tǒng)條件下是可行的闽颇,但當(dāng)你試圖去完成一個(gè)系統(tǒng)時(shí)盾戴,發(fā)現(xiàn)并不可行。好在JAVA已經(jīng)設(shè)計(jì)好了Executors來(lái)幫我們來(lái)管理線程兵多。
Java從1.5版本開(kāi)始尖啡,為簡(jiǎn)化多線程并發(fā)編程,引入全新的并發(fā)編程包:java.util.concurrent及其并發(fā)編程框架(Executor框架)剩膘。 Executor框架是指java 5中引入的一系列并發(fā)庫(kù)中與executor相關(guān)的一些功能類(lèi)衅斩,其中包括線程池,Executor怠褐,Executors畏梆,ExecutorService,CompletionService奈懒,F(xiàn)uture奠涌,Callable等。他們的關(guān)系為
并發(fā)編程的一種編程方式是把任務(wù)拆分為一系列的小任務(wù)磷杏,即Runnable铣猩,然后將這些任務(wù)提交給一個(gè)Executor執(zhí)行,Executor.execute(Runnalbe) 茴丰。Executor在執(zhí)行時(shí)使用其內(nèi)部的線程池來(lái)完成操作。 Executor的子接口有:ExecutorService,ScheduledExecutorService,已知實(shí)現(xiàn)類(lèi):AbstractExecutorService,ScheduledThreadPoolExecutor,ThreadPoolExecutor天吓。
4.2 線程池
newCachedThreadPool() | 緩存型池子贿肩,先查看池中有沒(méi)有以前建立的線程,如果有龄寞,就 reuse.如果沒(méi)有汰规,就建一個(gè)新的線程加入池中;緩存型池子通常用于執(zhí)行一些生存期很短的異步型任務(wù)因此在一些面向連接的daemon型SERVER中用得不多。但對(duì)于生存期短的異步任務(wù)物邑,它是Executor的首選溜哮。能reuse的線程,必須是timeout IDLE內(nèi)的池中線程色解,缺省 timeout是60s,超過(guò)這個(gè)IDLE時(shí)長(zhǎng)茂嗓,線程實(shí)例將被終止及移出池。注意科阎,放入CachedThreadPool的線程不必?fù)?dān)心其結(jié)束述吸,超過(guò)TIMEOUT不活動(dòng),其會(huì)自動(dòng)被終止锣笨。 |
newFixedThreadPool(int) | newFixedThreadPool與cacheThreadPool差不多蝌矛,也是能reuse就用道批,但不能隨時(shí)建新的線程;-其獨(dú)特之處:任意時(shí)間點(diǎn),最多只能有固定數(shù)目的活動(dòng)線程存在入撒,此時(shí)如果有新的線程要建立隆豹,只能放在另外的隊(duì)列中等待,直到當(dāng)前的線程中某個(gè)線程終止直接被移出池子;-和cacheThreadPool不同茅逮,F(xiàn)ixedThreadPool沒(méi)有IDLE機(jī)制(可能也有璃赡,但既然文檔沒(méi)提,肯定非常長(zhǎng)氮唯,類(lèi)似依賴(lài)上層的TCP或UDP IDLE機(jī)制之類(lèi)的)鉴吹,所以FixedThreadPool多數(shù)針對(duì)一些很穩(wěn)定很固定的正規(guī)并發(fā)線程,多用于服務(wù)器;-從方法的源代碼看惩琉,cache池和fixed 池調(diào)用的是同一個(gè)底層 池豆励,只不過(guò)參數(shù)不同:fixed池線程數(shù)固定,并且是0秒IDLE(無(wú)IDLE) cache池線程數(shù)支持0-Integer.MAX_VALUE(顯然完全沒(méi)考慮主機(jī)的資源承受能力)瞒渠,60秒IDLE |
newScheduledThreadPool(int) | 這個(gè)池子里的線程可以按schedule依次delay執(zhí)行良蒸,或周期執(zhí)行 |
SingleThreadExecutor() | -單例線程,任意時(shí)間池中只能有一個(gè)線程;-用的是和cache池和fixed池相同的底層池伍玖,但線程數(shù)目是1-1,0秒IDLE(無(wú)IDLE) |
4.3 隊(duì)列
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)
corePoolSize:線程池中所保存的核心線程數(shù)嫩痰,包括空閑線程。
maximumPoolSize:池中允許的最大線程數(shù)窍箍。
keepAliveTime:線程池中的空閑線程所能持續(xù)的最長(zhǎng)時(shí)間串纺。
unit:持續(xù)時(shí)間的單位。
workQueue:任務(wù)執(zhí)行前保存任務(wù)的隊(duì)列椰棘,僅保存由execute方法提交的Runnable任務(wù)纺棺。
根據(jù)ThreadPoolExecutor源碼前面大段的注釋?zhuān)覀兛梢钥闯觯?dāng)試圖通過(guò)excute方法講一個(gè)Runnable任務(wù)添加到線程池中時(shí)邪狞,按照如下順序來(lái)處理:
1祷蝌、如果線程池中的線程數(shù)量少于corePoolSize,即使線程池中有空閑線程帆卓,也會(huì)創(chuàng)建一個(gè)新的線程來(lái)執(zhí)行新添加的任務(wù)巨朦;
2、如果線程池中的線程數(shù)量大于等于corePoolSize剑令,但緩沖隊(duì)列workQueue未滿(mǎn)糊啡,則將新添加的任務(wù)放到workQueue中,按照FIFO的原則依次等待執(zhí)行(線程池中有線程空閑出來(lái)后依次將緩沖隊(duì)列中的任務(wù)交付給空閑的線程執(zhí)行)尚洽;
3悔橄、如果線程池中的線程數(shù)量大于等于corePoolSize,且緩沖隊(duì)列workQueue已滿(mǎn),但線程池中的線程數(shù)量小于maximumPoolSize癣疟,則會(huì)創(chuàng)建新的線程來(lái)處理被添加的任務(wù)挣柬;
4、如果線程池中的線程數(shù)量等于了maximumPoolSize睛挚,有4種才處理方式(該構(gòu)造方法調(diào)用了含有5個(gè)參數(shù)的構(gòu)造方法邪蛔,并將最后一個(gè)構(gòu)造方法為RejectedExecutionHandler類(lèi)型,它在處理線程溢出時(shí)有4種方式扎狱,這里不再細(xì)說(shuō)侧到,要了解的,自己可以閱讀下源碼)淤击。
總結(jié)起來(lái)匠抗,也即是說(shuō),當(dāng)有新的任務(wù)要處理時(shí)污抬,先看線程池中的線程數(shù)量是否大于corePoolSize汞贸,再看緩沖隊(duì)列workQueue是否滿(mǎn),最后看線程池中的線程數(shù)量是否大于maximumPoolSize印机。
另外矢腻,當(dāng)線程池中的線程數(shù)量大于corePoolSize時(shí),如果里面有線程的空閑時(shí)間超過(guò)了keepAliveTime射赛,就將其移除線程池多柑,這樣,可以動(dòng)態(tài)地調(diào)整線程池中線程的數(shù)量楣责。
我們大致來(lái)看下Executors的源碼竣灌,newCachedThreadPool的不帶. RejectedExecutionHandler參數(shù)(即第五個(gè)參數(shù),線程數(shù)量超過(guò)maximumPoolSize時(shí)秆麸,指定處理方式)的構(gòu)造方法如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
它將corePoolSize設(shè)定為0帐偎,而將maximumPoolSize設(shè)定為了Integer的最大值,線程空閑超過(guò)60秒蛔屹,將會(huì)從線程池中移除。由于核心線程數(shù)為0豁生,因此每次添加任務(wù)兔毒,都會(huì)先從線程池中找空閑線程,如果沒(méi)有就會(huì)創(chuàng)建一個(gè)線程(SynchronousQueue<Runnalbe>決定的甸箱,后面會(huì)說(shuō))來(lái)執(zhí)行新的任務(wù)育叁,并將該線程加入到線程池中,而最大允許的線程數(shù)為Integer的最大值芍殖,因此這個(gè)線程池理論上可以不斷擴(kuò)大豪嗽。
再來(lái)看newFixedThreadPool的不帶RejectedExecutionHandler參數(shù)的構(gòu)造方法,如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
它將corePoolSize和maximumPoolSize都設(shè)定為了nThreads,這樣便實(shí)現(xiàn)了線程池的大小的固定龟梦,不會(huì)動(dòng)態(tài)地?cái)U(kuò)大隐锭,另外,keepAliveTime設(shè)定為了0计贰,也就是說(shuō)線程只要空閑下來(lái)钦睡,就會(huì)被移除線程池,敢于LinkedBlockingQueue下面會(huì)說(shuō)躁倒。
下面說(shuō)說(shuō)幾種排隊(duì)的策略:
1荞怒、直接提交。緩沖隊(duì)列采用 SynchronousQueue秧秉,它將任務(wù)直接交給線程處理而不保持它們褐桌。如果不存在可用于立即運(yùn)行任務(wù)的線程(即線程池中的線程都在工作),則試圖把任務(wù)加入緩沖隊(duì)列將會(huì)失敗象迎,因此會(huì)構(gòu)造一個(gè)新的線程來(lái)處理新添加的任務(wù)荧嵌,并將其加入到線程池中。直接提交通常要求無(wú)界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒絕新提交的任務(wù)挖帘。newCachedThreadPool采用的便是這種策略完丽。
2、無(wú)界隊(duì)列拇舀。使用無(wú)界隊(duì)列(典型的便是采用預(yù)定義容量的 LinkedBlockingQueue逻族,理論上是該緩沖隊(duì)列可以對(duì)無(wú)限多的任務(wù)排隊(duì))將導(dǎo)致在所有 corePoolSize 線程都工作的情況下將新任務(wù)加入到緩沖隊(duì)列中。這樣骄崩,創(chuàng)建的線程就不會(huì)超過(guò) corePoolSize聘鳞,也因此,maximumPoolSize 的值也就無(wú)效了要拂。當(dāng)每個(gè)任務(wù)完全獨(dú)立于其他任務(wù)抠璃,即任務(wù)執(zhí)行互不影響時(shí),適合于使用無(wú)界隊(duì)列脱惰。newFixedThreadPool采用的便是這種策略搏嗡。
3、有界隊(duì)列拉一。當(dāng)使用有限的 maximumPoolSizes 時(shí)采盒,有界隊(duì)列(一般緩沖隊(duì)列使用ArrayBlockingQueue,并制定隊(duì)列的最大長(zhǎng)度)有助于防止資源耗盡蔚润,但是可能較難調(diào)整和控制磅氨,隊(duì)列大小和最大池大小需要相互折衷,需要設(shè)定合理的參數(shù)嫡纠。