1 前言
在JDK5之前,Java多線程以及其性能一直是個軟肋厚满,只有synchronized笙各、Thread.sleep()敷矫、Object.wait/notify這樣有限的方法,而synchronized的效率還特別地低低矮,開銷比較大印叁。
在JDK5之后,相對于前面版本有了重大改進军掂,不僅在Java語法上有了很多改進轮蜕,包括:泛型、裝箱蝗锥、for循環(huán)跃洛、變參等,在多線程上也有了徹底提高终议,其引進了并發(fā)編程大師Doug Lea的java.util.concurrent包(后面簡稱J.U.C)汇竭,支持了現(xiàn)代CPU的CAS原語,不僅在性能上有了很大提升穴张,在自由度上也有了更多的選擇细燎,此時 J.U.C的效率在高并發(fā)環(huán)境下的效率遠優(yōu)于synchronized。
在JDK6(Mustang 野馬)中皂甘,對synchronized的內(nèi)在機制做了大量顯著的優(yōu)化玻驻,加入了CAS的概念以及偏向鎖、輕量級鎖偿枕,使得synchronized的效率與J.U.C不相上下璧瞬,并且官方說后面該關(guān)鍵字還有繼續(xù)優(yōu)化的空間佛析,所以在現(xiàn) 在JDK7時代,synchronized已經(jīng)成為一般情況下的首選彪蓬,在某些特殊場景:可中斷的鎖寸莫、條件鎖、等待獲得鎖一段時間如果失敗則停止档冬,J.U.C是適用的膘茎,所以對于 多線程研究來說,了解其原理以及各自的適用場景是必要的酷誓。
2 基本概念
2.1 線程
線程是依附于進程的披坏,進程是分配資源的最小單位,一個進程可以生成多個線程盐数,這些線程擁有共享的進程資源棒拂。就每個線程而言,只有很少的獨有資源玫氢,如:控制線程運行的線程控制塊帚屉,保留局部變量和少數(shù)參數(shù)的棧空間等漾峡。線程有就緒攻旦、阻塞和運行三種狀態(tài),并可以在這之間切換生逸。也正因為多個線程會共享進程資源牢屋,所以當(dāng)它們對同一個共享變量/對象進行操作的時候,線程的沖突和不一致性就產(chǎn)生了槽袄。
多線程并發(fā)環(huán)境下烙无,本質(zhì)上要解決地是這兩個問題:
- 線程之間如何通信;
- 線程之間如何同步遍尺;
概括起來說就是:線程之間如何正確地通信截酷。雖然說的是在Java層面如何保證,但會涉及到 Java虛擬機狮鸭、Java內(nèi)存模型合搅,以及Java這樣的高級語言最終是要映射到CPU來執(zhí)行(關(guān)鍵原因:如今的CPU有緩存、并且是多核的)歧蕉,雖然有些難懂灾部,但對于深刻把握多線程是至關(guān)重要的,所以需要多花一些時間惯退。
2.2 鎖
當(dāng)多個線程對同一個共享變量/對象進行操作赌髓,即使是最簡單的操作,如:i++,在處理上實際也涉及到讀取锁蠕、自增夷野、賦值這三個操作,也就是說 這中間存在時間差荣倾,導(dǎo)致多個線程沒有按照如程序編寫者所設(shè)想的去順序執(zhí)行悯搔,出現(xiàn)錯位,從而導(dǎo)致最終結(jié)果與預(yù)期不一致舌仍。
Java中的多線程同步是通過鎖的概念來體現(xiàn)妒貌。鎖不是一個對象、不是一個具體的東西铸豁,而是一種機制的名稱灌曙。鎖機制需要保證如下兩種特性:
- 互斥性:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現(xiàn)多線程中的協(xié)調(diào)機制节芥,這樣在同一時間只有一個線程對需同步的代碼塊(復(fù)合操作)進行訪問在刺。互斥性我們也往往稱為操作的原子性;
- 可見性:必須確保在鎖被釋放之前头镊,對共享變量所做的修改蚣驼,對于隨后獲得該鎖的另一個線程是可見的(即在獲得鎖時應(yīng)獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續(xù)操作從而引起不一致拧晕;
2.3 掛起隙姿、休眠、阻塞與非阻塞
掛起(Suspend):當(dāng)線程被掛起的時候厂捞,其會失去CPU的使用時間,直到被其他線程(用戶線程或調(diào)度線程)喚醒队丝。
休眠(Sleep):同樣是會失去CPU的使用時間靡馁,但是在過了指定的休眠時間之后,它會自動激活机久,無需喚醒(整個喚醒表面看是自動的臭墨,但實際上也得有守護線程去喚醒,只是不需編程者手動干預(yù))膘盖。
阻塞(Block):在線程執(zhí)行時胧弛,所需要的資源不能得到,則線程被掛起侠畔,直到滿足可操作的條件结缚。
非阻塞(Block):在線程執(zhí)行時,所需要的資源不能得到软棺,則線程不是被掛起等待红竭,而是繼續(xù)執(zhí)行其余事情,待條件滿足了之后,收到了通知(同樣是守護線程去做)再執(zhí)行茵宪。
掛起和休眠是獨立的操作系統(tǒng)的概念最冰,而阻塞與非阻塞則是在資源不能得到時的兩種處理方式,不限于操作系統(tǒng)稀火,當(dāng)資源申請不到時暖哨,要么掛起線程等待、要么繼續(xù)執(zhí)行其他操作凰狞,資源被滿足后再通知該線程重新請求鹿蜀。顯然非阻塞的效率要高于阻塞,相應(yīng)的實現(xiàn)的復(fù)雜度也要高一些服球。
在Java中顯式的掛起之前是通過Thread的suspend方法來體現(xiàn)茴恰,現(xiàn)在此概念已經(jīng)消失,原因是suspend/resume方法已經(jīng)被廢棄斩熊,它們?nèi)菀桩a(chǎn)生死鎖往枣,在suspend方法的注釋里有這么一段話:當(dāng)suspend的線程持有某個對象鎖,而resume它的線程又正好需要使用此鎖的時候粉渠,死鎖就產(chǎn)生了分冈。
所以,現(xiàn)在的JDK版本中霸株,掛起是JVM的系統(tǒng)行為雕沉,程序員無需干涉。休眠的過程中也不會釋放鎖去件,但它一定會在某個時間后被喚醒坡椒,所以不會死鎖。現(xiàn)在我們所說的掛起尤溜,往往并非指編寫者的程序里主動掛起倔叼,而是由操作系統(tǒng)的線程調(diào)度器去控制。
所以宫莱,我們常常說的“線程在申請鎖失敗后會被掛起丈攒、然后等待調(diào)度”這樣有一定歧義,因為這里的“掛起”是操作系統(tǒng)級別的掛起授霸,其實是在申請資源失敗時的阻塞巡验,和Java中的線程的掛起(可能已經(jīng)獲得鎖,也可能沒有鎖碘耳,總之和鎖無關(guān))不是一個概念显设,很容易混淆,所以在后文中說的掛起藏畅,一般指的是操作系統(tǒng)的操作敷硅,而不是Thread中的suspend()功咒。
相應(yīng)地有必要提下java.lang.Object的wait/notify,這兩個方法同樣是等待/通知绞蹦,但它們的前提是已經(jīng)獲得了鎖力奋,且在wait(等待)期間會釋放鎖。在wait方法的注釋里明確提到:線程要調(diào)用wait方法幽七,必須先獲得該對象的鎖景殷,在調(diào)用wait之后,當(dāng)前線程釋放該對象鎖并進入休眠(這里到底是進入休眠還是掛起澡屡?文檔沒有細說猿挚,從該方法能指定等待時間來看,更可能是休眠驶鹉,沒有指定等待時間的绩蜻,則可能是掛起,不管如何室埋,在休眠/掛起之前办绝,JVM都會從當(dāng)前線程中把該對象鎖釋放掉),只有以下幾種情況下會被喚醒:其他線程調(diào)用了該對象的notify或notifyAll姚淆、當(dāng)前線程被中斷孕蝉、調(diào)用wait時指定的時間已到。
2.4 內(nèi)核態(tài)與用戶態(tài)
這是兩個操作系統(tǒng)的概念腌逢,但理解它們對我們理解Java的線程機制有著一定幫助降淮。
有一些系統(tǒng)級的調(diào)用,比如:清除時鐘搏讶、創(chuàng)建進程等這些系統(tǒng)指令佳鳖,如果這些底層系統(tǒng)級指令能夠被應(yīng)用程序任意訪問的話,那么后果是危險的窍蓝,系統(tǒng)隨時可能崩潰腋颠,所以 CPU將所執(zhí)行的指令設(shè)置為多個特權(quán)級別,在硬件執(zhí)行每條指令時都會校驗指令的特權(quán)吓笙,比如:Intel x86架構(gòu)的CPU將特權(quán)分為0-3四個特權(quán)級,0級的權(quán)限最高巾腕,3權(quán)限最低面睛。
而操作系統(tǒng)根據(jù)這系統(tǒng)調(diào)用的安全性分為兩種:內(nèi)核態(tài)和用戶態(tài)。內(nèi)核態(tài)執(zhí)行的指令的特權(quán)是0尊搬,用戶態(tài)執(zhí)行的指令的特權(quán)是3叁鉴。
- 當(dāng)一個任務(wù)(進程)執(zhí)行系統(tǒng)調(diào)用而進入內(nèi)核指令執(zhí)行時,進程處于內(nèi)核運行態(tài)(或簡稱為內(nèi)核態(tài))佛寿;
- 當(dāng)任務(wù)(進程)執(zhí)行自己的代碼時幌墓,進程就處于用戶態(tài)但壮;
明白了內(nèi)核態(tài)和用戶態(tài)的概念之后,那么在這兩種狀態(tài)之間切換會造成什么樣的效率影響常侣?
在執(zhí)行系統(tǒng)級調(diào)用時蜡饵,需要將變量傳遞進去、可能要拷貝胳施、計數(shù)社牲、保存一些上下文信息救拉,然后內(nèi)核態(tài)執(zhí)行完成之后需要再將參數(shù)傳遞到用戶進程中去,這個切換的代價相對來說是比較大的,所以應(yīng)該是 盡量避免頻繁地在內(nèi)核態(tài)和用戶態(tài)之間切換掂咒。
那操作系統(tǒng)的這兩種形態(tài)和我們的線程主題有什么關(guān)系呢?這里是關(guān)鍵皮仁。Java并沒有自己的線程模型魂爪,而是使用了操作系統(tǒng)的原生線程!
如果要實現(xiàn)自己的線程模型哩盲,那么有些問題就特別復(fù)雜前方,難以解決,比如:如何處理阻塞种冬、如何在多CPU之間合理地分配線程镣丑、如何鎖定,包括創(chuàng)建娱两、銷毀線程這些莺匠,都需要Java自己來做,在JDK1.2之前Java曾經(jīng)使用過自己實現(xiàn)的線程模型十兢,后來放棄了趣竣,轉(zhuǎn)向使用操作系統(tǒng)的線程模型,因此創(chuàng)建旱物、銷毀遥缕、調(diào)度、阻塞等這些事都交由操作系統(tǒng)來做宵呛,而 線程方面的事在操作系統(tǒng)來說屬于系統(tǒng)級的調(diào)用单匣,需要在內(nèi)核態(tài)完成,所以如果頻繁地執(zhí)行線程掛起宝穗、調(diào)度户秤,就會頻繁造成在內(nèi)核態(tài)和用戶態(tài)之間切換,影響效率(當(dāng)然逮矛,操作系統(tǒng)的線程操作是不允許外界(包括Java虛擬機)直接訪問的鸡号,而是開放了叫“輕量級進程”的接口供外界使用,其與內(nèi)核線程在Window和Linux上是一對一的關(guān)系须鼎,這里不多敘述)鲸伴。
前面說JDK5之前的synchronized效率低下府蔗,是 因為在阻塞時線程就會被掛起、然后等待重新調(diào)度汞窗,而線程操作屬于內(nèi)核態(tài)姓赤,這頻繁的掛起、調(diào)度使得操作系統(tǒng)頻繁處于內(nèi)核態(tài)和用戶態(tài)的轉(zhuǎn)換杉辙,造成頻繁的變量傳遞模捂、上下文保存等,從而性能較低蜘矢。
3 線程優(yōu)勢
盡管面臨很多挑戰(zhàn)狂男,多線程有一些優(yōu)點使得它一直被使用。這些優(yōu)點是:
- 資源利用率更好品腹;
- 程序設(shè)計在某些情況下更簡單岖食;
- 程序響應(yīng)更快速;
3.1 資源利用率更好
CPU能夠在等待IO的時候做一些其他的事情舞吭。這個不一定就是磁盤IO泡垃。它也可以是網(wǎng)絡(luò)的IO,或者用戶輸入羡鸥。通常情況下蔑穴,網(wǎng)絡(luò)和磁盤的IO比CPU和內(nèi)存的IO慢的多。
3.2 程序設(shè)計更簡單
在單線程應(yīng)用程序中惧浴,如果你想編寫程序手動處理多個IO的讀取和處理的順序存和,你必須記錄每個文件讀取和處理的狀態(tài)。相反衷旅,你可以啟動兩個線程捐腿,每個線程處理一個文件的讀取和處理操作。線程會在等待磁盤讀取文件的過程中被阻塞柿顶。在等待的時候茄袖,其他的線程能夠使用CPU去處理已經(jīng)讀取完的文件。其結(jié)果就是嘁锯,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中宪祥。這會帶來磁盤和CPU利用率的提升。而且每個線程只需要記錄一個文件家乘,因此這種方式也很容易編程實現(xiàn)品山。
3.3 程序響應(yīng)更快速
將一個單線程應(yīng)用程序變成多線程應(yīng)用程序的另一個常見的目的是 實現(xiàn)一個響應(yīng)更快的應(yīng)用程序。設(shè)想一個服務(wù)器應(yīng)用烤低,它在某一個端口監(jiān)聽進來的請求。當(dāng)一個請求到來時笆载,它去處理這個請求扑馁,然后再返回去監(jiān)聽涯呻。
如果一個請求需要占用大量的時間來處理,在這段時間內(nèi)新的客戶端就無法發(fā)送請求給服務(wù)端腻要。只有服務(wù)器在監(jiān)聽的時候复罐,請求才能被接收。
另一種設(shè)計是雄家,監(jiān)聽線程把請求傳遞給工作者線程池(worker thread pool)效诅,然后立刻返回去監(jiān)聽。而工作者線程則能夠處理這個請求并發(fā)送一個回復(fù)給客戶端趟济。
4 線程代價
使用多線程往往可以 獲得更大的吞吐率和更短的響應(yīng)時間乱投,但是,使用多線程不一定就比單線程程序跑的快顷编,這取決于我們程序設(shè)計者的能力以及應(yīng)用場景的不同戚炫。不要為了多線程而多線程,而應(yīng)考慮具體的應(yīng)用場景和開發(fā)實力媳纬,使用多線程就是希望能夠獲得更快的處理速度和利用閑置的處理能力双肤,如果沒帶來任何好處還帶來了復(fù)雜性和一些定時炸彈,那就傻逼了钮惠?只有在使用多線程給我們帶來的好處遠大于我們付出的代價時茅糜,才考慮使用多線程。有時候可能引入多線程帶來的性能提升抵不過多線程而引入的開銷素挽,一個沒有經(jīng)過良好并發(fā)設(shè)計得程序也可能比使用單線程還更慢蔑赘。
4.1 設(shè)計更復(fù)雜
多線程程序在訪問共享可變數(shù)據(jù)的時候往往需要我們很小心的處理,否則就會出現(xiàn)難以發(fā)現(xiàn)的BUG毁菱,一般地米死,多線程程序往往比單線程程序設(shè)計會更加復(fù)雜(盡管有些單線程處理程序可能比多線程程序要復(fù)雜),而且錯誤很難重現(xiàn)(因為線程調(diào)度的無序性贮庞,某些bug的出現(xiàn)依賴于某種特定的線程執(zhí)行時序)峦筒。
4.2 上下文切換開銷
當(dāng)CPU從執(zhí)行一個線程切換到執(zhí)行另外一個線程的時候,需要先存儲當(dāng)前線程的本地的數(shù)據(jù)窗慎,程序指針等物喷,然后載入另一個線程的本地數(shù)據(jù),程序指針等遮斥,最后才開始執(zhí)行峦失。這種切換稱為 “上下文切換”(“context switch”)。CPU會在一個上下文中執(zhí)行一個線程术吗,然后切換到另外一個上下文中執(zhí)行另外一個線程尉辑。
上下文切換并不廉價。如果沒有必要较屿,應(yīng)該減少上下文切換的發(fā)生隧魄。
4.3 增加資源消耗
線程在運行的時候需要從計算機里面得到一些資源卓练。除了CPU,線程還需要一些內(nèi)存來維持它本地的堆棧购啄。它也需要占用操作系統(tǒng)中一些資源來管理線程襟企。我們可以嘗試編寫一個程序,讓它創(chuàng)建100個線程狮含,這些線程什么事情都不做顽悼,只是在等待,然后看看這個程序在運行的時候占用了多少內(nèi)存几迄。
5 創(chuàng)建運行
編寫線程運行時執(zhí)行的代碼有兩種方式:一種是創(chuàng)建Thread子類的一個實例并重寫run方法蔚龙,第二種是創(chuàng)建類的時候?qū)崿F(xiàn)Runnable接口。
5.1 創(chuàng)建Thread的子類
創(chuàng)建Thread子類的一個實例并重寫run方法乓旗,run方法會在調(diào)用start()方法之后被執(zhí)行府蛇。例子如下:
public class MyThread extends Thread {
public void run(){
System.out.println("MyThread running");
}
}
可以用如下方式創(chuàng)建并運行上述Thread子類:
MyThread myThread = new MyThread();
myTread.start();
一旦線程啟動后start方法就會立即返回,而不會等待到run方法執(zhí)行完畢才返回屿愚。就好像run方法是在另外一個cpu上執(zhí)行一樣汇跨。當(dāng)run方法執(zhí)行后,將會打印出字符串MyThread running妆距。
5.2 實現(xiàn)Runnable接口
第二種編寫線程執(zhí)行代碼的方式是新建一個實現(xiàn)了java.lang.Runnable接口的類的實例穷遂,實例中的方法可以被線程調(diào)用。下面給出例子:
public class MyRunnable implements Runnable {
public void run(){
System.out.println("MyRunnable running");
}
}
為了使線程能夠執(zhí)行run()方法娱据,需要在Thread類的構(gòu)造函數(shù)中傳入 MyRunnable的實例對象蚪黑。示例如下:
Thread thread = new Thread(new MyRunnable());
thread.start();
當(dāng)線程運行時,它將會調(diào)用實現(xiàn)了Runnable接口的run方法中剩。上例中將會打印出”MyRunnable running”忌穿。
5.3 創(chuàng)建子類還是實現(xiàn)Runnable接口?
對于這兩種方式哪種好并沒有一個確定的答案结啼,它們都能滿足要求掠剑。就個人意見,更傾向于實現(xiàn)Runnable接口這種方法郊愧。因為線程池可以有效的管理實現(xiàn)了Runnable接口的線程朴译,如果線程池滿了,新的線程就會排隊等候執(zhí)行属铁,直到線程池空閑出來為止眠寿。而如果線程是通過實現(xiàn)Thread子類實現(xiàn)的,這將會復(fù)雜一些焦蘑。
有時我們要同時融合實現(xiàn)Runnable接口和Thread子類兩種方式盯拱。例如,實現(xiàn)了Thread子類的實例可以執(zhí)行多個實現(xiàn)了Runnable接口的線程。一個典型的應(yīng)用就是線程池坟乾。
5.4 常見錯誤:調(diào)用run()方法而非start()方法
創(chuàng)建并運行一個線程所犯的常見錯誤是調(diào)用線程的run()方法而非start()方法迹辐,如下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run(); //should be start();
起初你并不會感覺到有什么不妥,因為run()方法的確如你所愿的被調(diào)用了甚侣。但是,事實上间学,run()方法并非是由剛創(chuàng)建的新線程所執(zhí)行的殷费,而是被創(chuàng)建新線程的當(dāng)前線程所執(zhí)行了。也就是被執(zhí)行上面兩行代碼的線程所執(zhí)行的低葫。想要讓創(chuàng)建的新線程執(zhí)行run()方法详羡,必須調(diào)用新線程的start()方法。
5.5 線程名
當(dāng)創(chuàng)建一個線程的時候嘿悬,可以給線程起一個名字实柠。它有助于我們區(qū)分不同的線程。例如:如果有多個線程寫入System.out善涨,我們就能夠通過線程名容易的找出是哪個線程正在輸出窒盐。例子如下:
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "New Thread");
thread.start();
System.out.println(thread.getName());
需要注意的是,因為MyRunnable并非Thread的子類钢拧,所以MyRunnable類并沒有g(shù)etName()方法蟹漓。可以通過以下方式得到當(dāng)前線程的引用:
Thread.currentThread();
因此源内,通過如下代碼可以得到當(dāng)前線程的名字:
String threadName = Thread.currentThread().getName();
首先輸出執(zhí)行main()方法線程名字葡粒。這個線程JVM分配的。然后開啟10個線程膜钓,命名為1~10嗽交。每個線程輸出自己的名字后就退出。
public class ThreadExample {
public static void main(String[] args){
System.out.println(Thread.currentThread().getName());
for(int i=0; i<10; i++){
new Thread("" + i){
public void run(){
System.out.println("Thread: " + getName() + "running");
}
}.start();
}
}
}
需要注意的是颂斜,盡管啟動線程的順序是有序的夫壁,但是執(zhí)行的順序并非是有序的。也就是說焚鲜,1號線程并不一定是第一個將自己名字輸出到控制臺的線程掌唾。這是因為線程是并行執(zhí)行而非順序的。JVM和操作系統(tǒng)一起決定了線程的執(zhí)行順序忿磅,他和線程的啟動順序并非一定是一致的糯彬。
5.6 Main線程與子線程關(guān)系
-
Main線程是個非守護線程,不能設(shè)置成守護線程
這是因為葱她,Main線程是由Java虛擬機在啟動的時候創(chuàng)建的撩扒。main方法開始執(zhí)行的時候,主線程已經(jīng)創(chuàng)建好并在運行了。對于運行中的線程搓谆,調(diào)用Thread.setDaemon()會拋出異常Exception in thread "main" java.lang.IllegalThreadStateException炒辉。
-
Main線程結(jié)束,其他線程一樣可以正常運行
主線程泉手,只是個普通的非守護線程黔寇,用來啟動應(yīng)用程序,不能設(shè)置成守護線程斩萌;除此之外缝裤,它跟其他非守護線程沒有什么不同。主線程執(zhí)行結(jié)束颊郎,其他線程一樣可以正常執(zhí)行憋飞。
這樣其實是很合理的,按照操作系統(tǒng)的理論姆吭,進程是資源分配的基本單位榛做,線程是CPU調(diào)度的基本單位。對于CPU來說内狸,其實并不存在java的主線程和子線程之分检眯,都只是個普通的線程。進程的資源是線程共享的答倡,只要進程還在轰传,線程就可以正常執(zhí)行,換句話說線程是強依賴于進程的瘪撇。也就是說:
線程其實并不存在互相依賴的關(guān)系获茬,一個線程的死亡從理論上來說,不會對其他線程有什么影響倔既。
-
Main線程結(jié)束恕曲,其他線程也可以立刻結(jié)束,當(dāng)且僅當(dāng)這些子線程都是守護線程
Java虛擬機(相當(dāng)于進程)退出的時機是:虛擬機中所有存活的線程都是守護線程渤涌。只要還有存活的非守護線程虛擬機就不會退出佩谣,而是等待非守護線程執(zhí)行完畢;反之实蓬,如果虛擬機中的線程都是守護線程茸俭,那么不管這些線程的死活java虛擬機都會退出。
6 再聊并發(fā)與并行
并發(fā)和并行的區(qū)別就是一個處理器同時處理多個任務(wù)和多個處理器或者是多核的處理器同時處理多個不同的任務(wù)安皱。前者是邏輯上的同時發(fā)生(simultaneous)调鬓,而后者是物理上的同時發(fā)生。
并發(fā)性(concurrency)酌伊,又稱共行性腾窝,是指能處理多個同時性活動的能力,并發(fā)事件之間不一定要同一時刻發(fā)生。
并行(parallelism)是指同時發(fā)生的兩個并發(fā)事件虹脯,具有并發(fā)的含義驴娃,而并發(fā)則不一定并行。
來個比喻:并發(fā)和并行的區(qū)別就是一個人同時吃三個饅頭和三個人同時吃三個饅頭循集。
上圖反映了一個包含8個操作的任務(wù)在一個有兩核心的CPU中創(chuàng)建四個線程運行的情況唇敞。假設(shè)每個核心有兩個線程,那么每個CPU中兩個線程會交替并發(fā)暇榴,兩個CPU之間的操作會并行運算厚棵。單就一個CPU而言兩個線程可以解決線程阻塞造成的不流暢問題,其本身運行效率并沒有提高蔼紧,多CPU的并行運算才真正解決了運行效率問題,這也正是并發(fā)和并行的區(qū)別狠轻。