多線程的好處
盡管多線程有著許多挑戰(zhàn)刽严,但是多線程仍然在使用的原因在于多線程有著諸多好處。其中一些好處如下:
- 更好的資源利用
- 某些情況下更簡(jiǎn)單的程序設(shè)計(jì)
- 響應(yīng)性更好的程序
1.更好的資源利用
想象一個(gè)應(yīng)用從本地文件系統(tǒng)中讀取和執(zhí)行文件捣作。我們假設(shè)從磁盤(pán)中讀取一個(gè)文件需要5s,執(zhí)行它需要2s贡蓖。執(zhí)行兩個(gè)文件將花費(fèi)
5秒讀取文件A
2秒執(zhí)行文件A
5秒讀取文件B
2秒執(zhí)行文件B
----------------------
總共14秒
當(dāng)從磁盤(pán)中讀取一個(gè)文件時(shí)纺酸,大部分的CPU時(shí)間用來(lái)等待從磁盤(pán)讀取數(shù)據(jù)嘲碱。在那段時(shí)間金砍,CPU是非常空閑的麦锯,它可以用來(lái)做一些其他事恕稠。通過(guò)改變操作的順序,CPU可以得到更好的利用扶欣,看下這個(gè)順序:
5秒讀取文件A
5秒讀取文件B + 2秒執(zhí)行文件A
2秒執(zhí)行文件B
----------------------
總共12秒
CPU等待第一個(gè)文件被讀取完鹅巍,然后它開(kāi)始讀取第二個(gè)文件。在第二個(gè)文件被讀取的同時(shí)宵蛀,CPU執(zhí)行第一個(gè)文件。記住县貌,當(dāng)?shù)却募拇疟P(pán)中讀取時(shí)术陶,CPU是非常空閑的煤痕。
一般情況下梧宫,CPU可以在等待IO時(shí)做一些其他事接谨。IO不只是磁盤(pán)IO,網(wǎng)絡(luò)IO也一樣塘匣,或者是機(jī)器上一個(gè)用戶的輸入脓豪。網(wǎng)絡(luò)和磁盤(pán)IO比起CPU和內(nèi)存IO來(lái)說(shuō)是非常慢的。
2.更簡(jiǎn)單的程序設(shè)計(jì)
如果你要在一個(gè)單線程應(yīng)用中編寫(xiě)上面順序的文件讀取和執(zhí)行過(guò)程忌卤,你需要記錄每個(gè)文件的讀取和執(zhí)行狀態(tài)扫夜。作為代替,你可以啟動(dòng)兩個(gè)線程驰徊,每個(gè)線程只讀取和執(zhí)行一個(gè)文件笤闯。當(dāng)?shù)却疟P(pán)讀取文件時(shí)線程會(huì)被阻塞,在等待時(shí)棍厂,另一個(gè)線程可以使用CPU來(lái)執(zhí)行它們已經(jīng)讀取的文件颗味。這將導(dǎo)致磁盤(pán)時(shí)刻保持忙碌,讀取各種文件到內(nèi)存中牺弹。這也導(dǎo)致磁盤(pán)和CPU的利用率更高浦马。它也很容易編程,因?yàn)槊總€(gè)線程只和一個(gè)文件有關(guān)聯(lián)张漂。
3.響應(yīng)性更好的程序
將單線程應(yīng)用轉(zhuǎn)變?yōu)槎嗑€程應(yīng)用的另一個(gè)目的是獲取更好的響應(yīng)晶默。想象一個(gè)服務(wù)器應(yīng)用,它監(jiān)聽(tīng)一些端口等待請(qǐng)求到來(lái)鹃锈。當(dāng)一個(gè)請(qǐng)求到達(dá)荤胁,它將處理請(qǐng)求然后繼續(xù)監(jiān)聽(tīng)。這個(gè)服務(wù)的循環(huán)簡(jiǎn)單表述如下:
while(服務(wù)器是活動(dòng)的) {
監(jiān)聽(tīng)請(qǐng)求
執(zhí)行請(qǐng)求
}
如果一個(gè)請(qǐng)求要花費(fèi)很多時(shí)間執(zhí)行屎债,在那期間將沒(méi)有新的客戶端可以發(fā)送請(qǐng)求給服務(wù)器仅政,只有當(dāng)服務(wù)器處于監(jiān)聽(tīng)狀態(tài)時(shí)才能夠接受請(qǐng)求。
一個(gè)代替的設(shè)計(jì)就是監(jiān)聽(tīng)線程將請(qǐng)求提交給工作線程來(lái)執(zhí)行盆驹,然后立刻返回繼續(xù)監(jiān)聽(tīng)圆丹。工作線程將會(huì)執(zhí)行請(qǐng)求然后發(fā)送一個(gè)回復(fù)給客戶端。這個(gè)設(shè)計(jì)如下:
while(服務(wù)器是活動(dòng)的) {
監(jiān)聽(tīng)請(qǐng)求
將請(qǐng)求提交給工作線程
}
通過(guò)這種方式服務(wù)器將會(huì)很快回到監(jiān)聽(tīng)狀態(tài)躯喇,因此更多客戶端可以發(fā)送請(qǐng)求給服務(wù)器辫封。這個(gè)服務(wù)器的響應(yīng)性會(huì)變得更好羡宙。
對(duì)于桌面應(yīng)用來(lái)說(shuō)是一樣的绍移。如果你點(diǎn)擊一個(gè)按鈕啟動(dòng)一個(gè)長(zhǎng)任務(wù),然后線程執(zhí)行這個(gè)任務(wù)更新窗口药蜻,按鈕等正压,當(dāng)任務(wù)執(zhí)行時(shí)這個(gè)應(yīng)用將會(huì)出現(xiàn)無(wú)響應(yīng)的情況欣福。作為代替,這個(gè)任務(wù)可以提交給一個(gè)工作線程執(zhí)行焦履。當(dāng)工作線程在執(zhí)行任務(wù)時(shí)拓劝,窗口線程仍然能夠響應(yīng)其他用戶請(qǐng)求雏逾。當(dāng)工作線程完成任務(wù),它將通知窗口線程郑临,接下來(lái)窗口線程可以使用此任務(wù)的結(jié)果來(lái)更新應(yīng)用窗口栖博。使用工作線程設(shè)計(jì)的程序?qū)τ脩魜?lái)說(shuō)顯得響應(yīng)性更好。
多線程的代價(jià)
從單線程到多線程應(yīng)用并不只帶來(lái)好處厢洞,它也有一些代價(jià)仇让。不要只因?yàn)槟隳茏龅骄驮趹?yīng)用中使用多線程。你應(yīng)當(dāng)知道這樣做之后的好處是否大于它所帶來(lái)的開(kāi)銷犀变。當(dāng)存在疑惑時(shí)妹孙,嘗試測(cè)試應(yīng)用的性能或響應(yīng)能力而不是僅僅做猜想。
1.更復(fù)雜的設(shè)計(jì)
雖然某些情況下多線程應(yīng)用比單線程應(yīng)用更簡(jiǎn)單获枝,但是在其他情況下會(huì)更復(fù)雜蠢正。多線程執(zhí)行的代碼獲取共享數(shù)據(jù)需要特殊的關(guān)注,線程間的交互也不永遠(yuǎn)是簡(jiǎn)單的省店。不正確的線程同步引發(fā)的問(wèn)題也更加難以檢測(cè)嚣崭,再次出現(xiàn)以及處理。
下面展示一段未進(jìn)行線程同步的簡(jiǎn)單代碼:
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.Thread.sleep;
public class MultiThreadsCostTest {
//非同步計(jì)數(shù)器
private int count;
//同步計(jì)數(shù)器
private AtomicInteger atomicCount = new AtomicInteger();
//累加次數(shù)
private static final int NUM = 10000;
@Test
public void test() throws Exception {
//啟動(dòng)四個(gè)線程進(jìn)行累加操作
for(int i = 0; i < 4; ++i) {
new Thread(() -> {
for(int j = 0; j < NUM; ++j ) {
count++;
}
}).start();
new Thread(() -> {
for(int j = 0; j < NUM; ++j ) {
atomicCount.getAndIncrement();
}
}).start();
}
//等待累加完成
sleep(3000);
System.out.println("count: " + count);
System.out.println("atomicCount: " + atomicCount.get());
}
}
運(yùn)行兩次懦傍,結(jié)果如下:
從圖中我們可以看出雹舀,未進(jìn)行同步的累加產(chǎn)生的結(jié)果時(shí)錯(cuò)誤的,并且每次結(jié)果很難會(huì)再次相同(可以進(jìn)行更多次測(cè)試論證)粗俱,所以不正確的線程同步引發(fā)的問(wèn)題也更加難以檢測(cè)说榆,再次出現(xiàn)以及處理。
2.頻繁的上下文切換
即使是單核處理器也支持多線程執(zhí)行代碼寸认,CPU給每個(gè)線程分配時(shí)間片來(lái)實(shí)現(xiàn)這個(gè)機(jī)制签财。時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間片非常短偏塞,所以CPU通過(guò)不停的切換線程執(zhí)行唱蒸,讓我們感覺(jué)多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒灸叼。
當(dāng)一個(gè)CPU從執(zhí)行一個(gè)線程切換到另一個(gè)線程時(shí)神汹,這個(gè)CPU需要保存當(dāng)前線程的本地?cái)?shù)據(jù),程序指針等古今,并且加載下一個(gè)要執(zhí)行的線程的數(shù)據(jù)和程序指針等屁魏。這個(gè)切換過(guò)程被稱作為“上下文切換”,CPU從執(zhí)行一個(gè)線程的上下文到執(zhí)行另一個(gè)線程的上下文捉腥。
就像是我們同時(shí)讀兩本書(shū)氓拼,當(dāng)我們?cè)谧x一本英文的技術(shù)書(shū)時(shí),發(fā)現(xiàn)某個(gè)單詞不認(rèn)識(shí),于是便打開(kāi)中英文字典披诗,但是在放下英文技術(shù)書(shū)之前,大腦必須先記下這本書(shū)讀到了多少頁(yè)多少行立磁,等查完單詞后呈队,能夠繼續(xù)讀這本書(shū)。這樣的切換是會(huì)影響讀書(shū)效率的唱歧,同時(shí)上下文切換也會(huì)影響多線程的執(zhí)行速度宪摧。
上下文切換并不是廉價(jià)的,你不會(huì)想要在多個(gè)線程中頻繁切換颅崩。
你可以在維基百科中獲取更多關(guān)于上下文切換的知識(shí):
http://en.wikipedia.org/wiki/Context_switch 英文版
https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E4%BA%A4%E6%8F%9B 中文版
多線程一定快么几于?
下面演示串行和并發(fā)執(zhí)行累加操作的時(shí)間,請(qǐng)分析:下面的并發(fā)執(zhí)行一定比串行執(zhí)行快么沿后?
public class ConcurrencyTest {
private static final long COUNT = 10000L;
public static void main(String[] args) throws InterruptedException {
serial();
concurrency();
}
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(() -> {
int a = 0;
for(long i = 0; i < COUNT; ++i) {
a += 5;
}
});
thread.start();
int b = 0;
for(long i = 0; i < COUNT; ++i) {
b--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("concurrency: " + time + "ms, b=" + b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for(long i = 0; i < COUNT; ++i) {
a += 5;
}
int b = 0;
for(long i = 0; i < COUNT; ++i) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial: " + time + "ms, b=" + b + ", a=" + a);
}
}
循環(huán)次數(shù) | 串行執(zhí)行耗時(shí)/ms | 并發(fā)執(zhí)行耗時(shí)/ms | 并發(fā)比串行快多少 |
---|---|---|---|
1w | 0 | 6 | 慢 |
10w | 2 | 7 | 慢 |
100w | 5 | 8 | 差不多 |
1000w | 19 | 11 | 快 |
1億 | 111 | 74 | 快 |
從上表中我們可以看出并發(fā)執(zhí)行并不是一定比串行執(zhí)行快沿彭,因?yàn)榫€程有創(chuàng)建和上下文切換的開(kāi)銷。
3.更多的資源開(kāi)銷
一個(gè)線程為了運(yùn)行需要從計(jì)算機(jī)中獲取一些資源尖滚。除了CPU時(shí)間喉刘,一個(gè)線程還需要一些內(nèi)存空間來(lái)保存它的本地堆棧。它可能還會(huì)占據(jù)一些在操作系統(tǒng)內(nèi)部管理線程的資源漆弄。嘗試寫(xiě)一個(gè)創(chuàng)建100個(gè)只等待不做事的線程的程序睦裳,然后查看這個(gè)應(yīng)用運(yùn)行時(shí)占用了多少內(nèi)存。
import org.junit.Test;
import static java.lang.Thread.sleep;
public class Test1 {
@Test
public void threadMemoryTest() throws Exception {
for(int i = 0; i < 1000; ++i) {
new Thread(() -> {
try {
//only wait
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
sleep(4000);
System.gc();
}
}
加入-verbose:gc 虛擬機(jī)參數(shù)撼唾,觀測(cè)結(jié)果:
[0.033s][info][gc] Using G1[4.591s][info][gc] GC(0) Pause Full (System.gc()) 7M->2M(14M) 24.702ms
第一個(gè)為在堆中占據(jù)的內(nèi)存廉邑,第二個(gè)是任務(wù)管理器中此java應(yīng)用占據(jù)的內(nèi)存〉构龋可以自己改變創(chuàng)建線程的數(shù)量觀測(cè)蛛蒙。
并發(fā)模型
并發(fā)系統(tǒng)可以使用不同的并發(fā)模型來(lái)實(shí)現(xiàn)。一個(gè)并發(fā)模型 指定了系統(tǒng)中的線程如何協(xié)作來(lái)完成它們的工作恨锚。不同的并發(fā)模型使用不同的方法劃分工作宇驾,然后線程間可以以不同的方式來(lái)通信和協(xié)作。這個(gè)并發(fā)模型教程將會(huì)深入現(xiàn)在最流行的在使用中的并發(fā)模型(2015)猴伶。
并發(fā)模型和分布式系統(tǒng)的相似點(diǎn)
本文中描述的并發(fā)模型和分布式系統(tǒng)使用的各種架構(gòu)相似课舍。在一個(gè)并發(fā)系統(tǒng)中不同的線程彼此通信,在一個(gè)分布式系統(tǒng)中不同的進(jìn)程(可能在不同的計(jì)算機(jī)中)彼此通信他挎。線程和進(jìn)程在本質(zhì)上非常相似筝尾,這也是為什么各種不同的并發(fā)模型經(jīng)常看起來(lái)和各種不同的分布式系統(tǒng)架構(gòu)差不多办桨。
當(dāng)然分布式系統(tǒng)擁有其他的挑戰(zhàn)筹淫,例如網(wǎng)絡(luò)可能出問(wèn)題,或者遠(yuǎn)程計(jì)算機(jī)呢撞、進(jìn)程停止運(yùn)行等损姜。但是一個(gè)運(yùn)行在大的服務(wù)器上的并發(fā)系統(tǒng)也可能會(huì)遇到相似的問(wèn)題饰剥,例如CPU壞了,網(wǎng)卡問(wèn)題摧阅,磁盤(pán)問(wèn)題等汰蓉。這種問(wèn)題出現(xiàn)的可能性很低,但是在理論上仍然可能出現(xiàn)棒卷。
因?yàn)椴l(fā)模型和分布式系統(tǒng)很相似顾孽,它們經(jīng)常相互借鑒一些想法。例如比规,在工作者(線程)中分配工作的模型和分布式系統(tǒng)中的負(fù)載均衡 load balancing in distributed systems模型相似若厚。錯(cuò)誤處理技術(shù)如日志,故障切換(fail-over)蜒什,作業(yè)冪等性(idempotency of jobs)等也是相似的测秸。
并行工作者 parallel worker
第一個(gè)并發(fā)模型我稱它為并行工作者 (parallel worker)模型,作業(yè)被分配給不同的工作者灾常。這里是描述并行工作者并發(fā)模型的圖:
在并行工作者并發(fā)模型中乞封,一個(gè)委托者將到來(lái)的工作分配給不同的工作者。每個(gè)工作者完成整個(gè)工作過(guò)程岗憋。工作者們并行工作肃晚,在不同的線程上運(yùn)行,也可能在不同的CPU上仔戈。
如果并行工作者并發(fā)模型在一個(gè)汽車工廠中實(shí)現(xiàn)关串,那么就是一個(gè)工作者生產(chǎn)一輛車。工作者將會(huì)生產(chǎn)指定的車輛监徘,并且從頭到尾完成所有生產(chǎn)過(guò)程晋修。
并行工作者并發(fā)模型一般在Java應(yīng)用中大量使用(雖然正在改變中)。許多在J.U.C java.util.concurrent Java package 中的并發(fā)組件都使用這個(gè)模型來(lái)設(shè)計(jì)凰盔。你也可以看到在Java企業(yè)版本應(yīng)用服務(wù)中這個(gè)模型的使用墓卦。
并行工作者優(yōu)點(diǎn)
并行工作者并發(fā)模型的優(yōu)點(diǎn)是它很容易理解。為了增加應(yīng)用的并行性户敬,你只需要增加更多的工作者落剪。
例如,如果你正在實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)爬蟲(chóng)尿庐,你可以使用不同數(shù)量的工作者爬取某個(gè)數(shù)量的頁(yè)面然后查看使用哪個(gè)數(shù)量的工作者可以獲取最短的時(shí)間(意味著最高的性能)忠怖。因?yàn)榫W(wǎng)絡(luò)爬蟲(chóng)是一個(gè)IO密集型工作,你可能會(huì)在你的計(jì)算機(jī)上最終采用每個(gè)CPU/核心上只有很少幾個(gè)線程的模式抄瑟。如果每個(gè)CPU上只有一個(gè)線程就太少了凡泣,因?yàn)樗鼘?huì)有很多空閑時(shí)間等待數(shù)據(jù)下載。
并行工作者缺點(diǎn)
并行工作者并發(fā)模型在簡(jiǎn)單的表面下潛藏了一些缺點(diǎn)。我將在下面的部分說(shuō)明最明顯的缺點(diǎn)鞋拟。
1. 共享狀態(tài)會(huì)變復(fù)雜
事實(shí)上并行工作者并發(fā)模型不像上面說(shuō)的那么簡(jiǎn)單骂维。工作者經(jīng)常需要共享某些類型的數(shù)據(jù),這些數(shù)據(jù)或者在內(nèi)存中或者在共享的數(shù)據(jù)庫(kù)中贺纲。下面的圖展示了這將如何使并行工作者并發(fā)模型復(fù)雜化:
在通信機(jī)制中某些共享狀態(tài)(shared state)是任務(wù)隊(duì)列席舍,但是其他種類的共享狀態(tài)如企業(yè)數(shù)據(jù),數(shù)據(jù)緩存哮笆,數(shù)據(jù)庫(kù)連接池等。
只要并發(fā)工作者并發(fā)模型中混雜了共享狀態(tài)汰扭,它將開(kāi)始變得復(fù)雜稠肘。線程需要以某種方式獲取共享狀態(tài)然后確保共享狀態(tài)被修改后對(duì)其他線程可見(jiàn)(送入主內(nèi)存中而不是只保存在修改線程的CPU緩存中)。線程需要避免競(jìng)爭(zhēng)條件 race conditions, 死鎖 deadlock 和許多其他共享狀態(tài)引發(fā)的并發(fā)問(wèn)題萝毛。
另外项阴,當(dāng)訪問(wèn)并發(fā)數(shù)據(jù)結(jié)構(gòu)時(shí),多個(gè)線程會(huì)彼此等待笆包,導(dǎo)致并行狀態(tài)會(huì)部分丟失环揽,因?yàn)檫@部分線程將暫停執(zhí)行。許多并發(fā)數(shù)據(jù)結(jié)構(gòu)是阻塞的庵佣,這意味只有一個(gè)或有限數(shù)量的線程可以訪問(wèn)它們歉胶,這會(huì)導(dǎo)致線程在這些共享數(shù)據(jù)結(jié)構(gòu)上競(jìng)爭(zhēng)。高競(jìng)爭(zhēng)將會(huì)在根本上影響訪問(wèn)共享數(shù)據(jù)結(jié)構(gòu)的代碼執(zhí)行的序列程度(degree of serialization of execution)巴粪,因?yàn)楹芏嗑€程因無(wú)法訪問(wèn)到共享數(shù)據(jù)而被暫停通今。
現(xiàn)代的非阻塞并發(fā)算法 non-blocking concurrency algorithms 可以降低競(jìng)爭(zhēng)并且提高性能,但是非阻塞算法很難實(shí)現(xiàn)肛根。
持久化數(shù)據(jù)結(jié)構(gòu)(Persistent data structures)是另一種代替方法辫塌。一個(gè)持久化數(shù)據(jù)結(jié)構(gòu)在修改時(shí)總是會(huì)保存它自己先前的版本。因此派哲,如果多個(gè)線程引用相同的持久化數(shù)據(jù)結(jié)構(gòu)然后一個(gè)線程修改它臼氨,修改線程獲得一個(gè)指向新的結(jié)構(gòu)的引用。所有的其他線程依然指向舊的仍然未變的結(jié)構(gòu)芭届,因此保持了一致性储矩。Scala編程語(yǔ)言擁有幾個(gè)持久化的數(shù)據(jù)結(jié)構(gòu)。
雖然持久化數(shù)據(jù)結(jié)構(gòu)是解決共享數(shù)據(jù)結(jié)構(gòu)的并發(fā)修改問(wèn)題的一個(gè)優(yōu)雅的方法褂乍,但是持久化數(shù)據(jù)結(jié)構(gòu)往往表現(xiàn)得并不那么好椰苟。
例如,一個(gè)持久化 list 在它的頭部增加所有的新元素树叽,然后返回一個(gè)新增加元素的引用舆蝴。所有其他的線程仍然持有一個(gè)指向未修改列表的第一個(gè)元素的引用,對(duì)于這些線程來(lái)說(shuō)列表看起來(lái)并未改變,它們不會(huì)看到新增加的元素洁仗。
這樣一個(gè)持久化列表被實(shí)現(xiàn)為一個(gè)鏈表层皱,不幸的是鏈表在現(xiàn)代硬件上表現(xiàn)得并不好。在列表上的每個(gè)元素都是一個(gè)不同的對(duì)象赠潦,這些對(duì)象會(huì)被分散到計(jì)算機(jī)內(nèi)存的各個(gè)地方〗信郑現(xiàn)代CPU在順序獲取數(shù)據(jù)上更快,所以在現(xiàn)代硬件上你使用數(shù)組實(shí)現(xiàn)列表會(huì)得到很大的性能提升(參考LinkedList與ArrayList)她奥。一個(gè)數(shù)組順序存儲(chǔ)數(shù)據(jù)瓮增。CPU緩存可以同時(shí)加載很大的數(shù)組到到緩存中,一旦加載完可以讓CPU直接從緩存中獲取數(shù)據(jù)哩俭。這對(duì)于數(shù)據(jù)分布在RAM各個(gè)地方的鏈表來(lái)說(shuō)是不可能的绷跑。
2. 無(wú)狀態(tài)工作者 Stateless Workers
共享狀態(tài)可以被系統(tǒng)中的其他線程修改,因此工作者必須在每次需要它的時(shí)候重新讀取共享狀態(tài)凡资,來(lái)確保自己在最新的拷貝副本上工作砸捏。這是正確的,不管共享狀態(tài)保存在內(nèi)存中還是在數(shù)據(jù)庫(kù)中隙赁。一個(gè)工作者不會(huì)在自己內(nèi)部保存狀態(tài)(但是在每次需要的時(shí)候都重讀它)被稱作是無(wú)狀態(tài)的 (stateless)垦藏。
每當(dāng)需要的時(shí)候重讀數(shù)據(jù)會(huì)使程序變慢,尤其是狀態(tài)存儲(chǔ)在外部的數(shù)據(jù)庫(kù)中時(shí)伞访。
3. 任務(wù)順序是不確定的
另一個(gè)并行工作者模型的缺點(diǎn)是任務(wù)執(zhí)行順序是不確定的掂骏,沒(méi)有辦法保證什么任務(wù)先被執(zhí)行或后被執(zhí)行。任務(wù)A可能在任務(wù)B前先提交給工作者厚掷,但是任務(wù)B可能會(huì)在任務(wù)A前執(zhí)行芭挽。
@Test
public void test2() throws Exception {
final int num = 10;
Thread[] threads = new Thread[num];
// 創(chuàng)建10個(gè)線程,只打印自己的名字
for(int i = 0; i < num; ++i) {
threads[i] = new Thread(() -> System.out.println(Thread.currentThread().getName()));
}
for(int i = 0; i < num; ++i) {
threads[i].start();
}
sleep(3000);
}
上面代碼啟動(dòng)了10個(gè)線程蝗肪,分別輸出自己的名字袜爪。將它們保存到數(shù)組中,然后按順序啟動(dòng)薛闪,結(jié)果如下:
可見(jiàn)即使線程按一種順序啟動(dòng)辛馆,執(zhí)行的順序也是不確定的,不一定和啟動(dòng)的順序相同豁延。
并行工作者模型不確定的特性使得很難在某個(gè)點(diǎn)及時(shí)推導(dǎo)系統(tǒng)的狀態(tài)昙篙,它也很難去保證一個(gè)任務(wù)在另一個(gè)任務(wù)前發(fā)生(不是不可能)。
下面演示一種保證任務(wù)發(fā)生順序的例子:
public class Join {
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();
for(int i = 0; i < 10; ++i) {
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
sleep(5000);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
private static class Domino implements Runnable {
private Thread thread;
Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
運(yùn)行結(jié)果如下:
note:這些例子僅用來(lái)證明上面的觀點(diǎn)诱咏,線程如果使用將在后面部分說(shuō)明苔可。
流水線 Assembly Line
第二個(gè)并發(fā)模型我稱它為流水線 (assembly line)并發(fā)模型。我選擇這個(gè)名字只是為了和前面的"并行工作者"意義兼容袋狞。其他的開(kāi)發(fā)者使用其他的名字(例如 響應(yīng)式系統(tǒng)焚辅,或者事件驅(qū)動(dòng)系統(tǒng))映屋,這依賴于不同的平臺(tái)/社區(qū)。這是一張描述流水線并發(fā)模型的圖:
在流水線并發(fā)模型中工作者被組織的像是在工廠流水線上的工人同蜻,每個(gè)工作者只會(huì)完成所有工作的一部分棚点,當(dāng)工作的某個(gè)部分被當(dāng)前工作者完成后就被推向下一個(gè)工作者完成后面的部分。
每個(gè)工作者在它自己的線程上運(yùn)行湾蔓,并且不和其他的工作者共享狀態(tài)瘫析。這也有時(shí)作為一個(gè)非共享 (shared nothing)的并發(fā)模型被提及。
使用流水線并發(fā)模型的系統(tǒng)經(jīng)常使用非阻塞IO來(lái)設(shè)計(jì)默责。非阻塞IO意味著當(dāng)一個(gè)工作者啟動(dòng)一個(gè)IO操作(例如讀取一個(gè)文件或從網(wǎng)絡(luò)連接中讀取數(shù)據(jù))后贬循,工作者不會(huì)等待IO調(diào)用完成。IO操作是緩慢的桃序,所以等待IO操作完成會(huì)浪費(fèi)CPU時(shí)間杖虾。CPU可以同時(shí)做一些其他的事情。當(dāng)一個(gè)IO操作完成葡缰,IO操作的結(jié)果(例如數(shù)據(jù)讀取或數(shù)據(jù)狀態(tài)寫(xiě)入)被提交給另一個(gè)工作者做后續(xù)的事情。
使用非阻塞IO忱反,IO操作決定了工作者的邊界泛释。一個(gè)工作者盡量做它可以做的事直到它需要啟動(dòng)一個(gè)IO操作,然后它放棄控制這個(gè)任務(wù)温算。當(dāng)IO操作完成后怜校,在流水線上的下一個(gè)工作者繼續(xù)在這個(gè)任務(wù)上工作,直到它也需要啟動(dòng)一個(gè)IO操作注竿。
事實(shí)上茄茁,作業(yè)可能不止在一個(gè)流水線上流動(dòng)。因?yàn)榇蠖鄶?shù)系統(tǒng)可以執(zhí)行不止一個(gè)作業(yè)巩割,因此作業(yè)會(huì)根據(jù)需要完成的工作在工作人員間流動(dòng)裙顽,事實(shí)上也有多個(gè)不同的虛擬流水線同時(shí)運(yùn)作。下面是作業(yè)如何在流水線系統(tǒng)上流動(dòng)的:
作業(yè)為了并發(fā)執(zhí)行甚至可以被提交給不止一個(gè)工作者宣谈。例如愈犹,一個(gè)作業(yè)可以同時(shí)被提交給一個(gè)作業(yè)執(zhí)行器和一個(gè)作業(yè)日志記錄器。這個(gè)圖描述了三個(gè)流水線如何將他們的工作提交給一個(gè)相同的工作者(在中間流水線上的最后一個(gè)工作者)來(lái)完成:
流水線還可以變得比這個(gè)更加復(fù)雜闻丑。
響應(yīng)式漩怎,事件驅(qū)動(dòng)系統(tǒng)
使用流水線并發(fā)模型的系統(tǒng)有些也會(huì)被稱作響應(yīng)式系統(tǒng),或者事件驅(qū)動(dòng)系統(tǒng)嗦嗡。這個(gè)系統(tǒng)的工作者響應(yīng)發(fā)生在系統(tǒng)中的事件勋锤,或者接受從外部世界或其他工作者提交的事件。例如一個(gè)HTTP請(qǐng)求侥祭,或者某個(gè)文件已經(jīng)加載到內(nèi)存的信號(hào)等叁执。
在撰寫(xiě)本文的時(shí)候(2015)茄厘,有許多有趣的響應(yīng)式/事件驅(qū)動(dòng)平臺(tái)可獲得,并且在未來(lái)會(huì)有更多平臺(tái)出現(xiàn)徒恋。一些最知名的例如:
- Vert.x
- Akka
- Node.JS (JavaScript)
個(gè)人來(lái)說(shuō)蚕断,我覺(jué)得Vert.x是最有趣的(尤其對(duì)一個(gè)像我一樣的Java/JVM守舊者)。
Actors vs. Channels
Actors和channels是兩個(gè)和流水線模型相似的例子入挣。
在actor模型中每個(gè)工作者被稱作一個(gè)actor亿乳。Actors之間可以彼此直接發(fā)送消,径筏。消息被發(fā)送并且異步執(zhí)行葛假。Actors可以被用作實(shí)現(xiàn)一個(gè)或多個(gè)作業(yè)執(zhí)行流水線,像之前描述的那樣滋恬。這里是一個(gè)描述actor模型的圖:
在channel [管道]模型中聊训,工作者不能彼此直接通信。代替的是它們?cè)诓煌墓艿乐邪l(fā)布它們的消息(活動(dòng))带斑。其他工作者可以監(jiān)聽(tīng)這些管道上的消息挂滓,這不需要發(fā)送者知道誰(shuí)正在監(jiān)聽(tīng)贝椿。這是一個(gè)描述channel模型的圖:
在寫(xiě)作的時(shí)候叭首,channel模型對(duì)我來(lái)說(shuō)更加靈活眷唉。一個(gè)工作者不需要知道哪個(gè)工作者在之后執(zhí)行作業(yè)党饮,它只需要知道在哪個(gè)管道上提交作業(yè)(或者發(fā)送消息等)蹲堂。管道上的監(jiān)聽(tīng)者工作與否不會(huì)影響工作者寫(xiě)入信息到管道中朽基,這允許工作者間某種程度上的低耦合枕磁。
流水線優(yōu)點(diǎn)
流水線并發(fā)模型比起并行工作者模型有幾個(gè)優(yōu)點(diǎn)沦寂。我將在下面的部分談?wù)搸讉€(gè)最大的優(yōu)點(diǎn)彤守。
1. 不共享狀態(tài) No Shared State
工作者之間不共享狀態(tài)的事實(shí)意味它們可以在實(shí)現(xiàn)時(shí)筝蚕,無(wú)需考慮所有由并發(fā)訪問(wèn)狀態(tài)引發(fā)的并發(fā)問(wèn)題,這將使工作者很容易被實(shí)現(xiàn)。你可以實(shí)現(xiàn)一個(gè)工作者卸奉,只要它是執(zhí)行那個(gè)工作的唯一的工作者 -- 根本上是一個(gè)單線程實(shí)現(xiàn)疹鳄。
2. 有狀態(tài)的工作者 Stateful Workers
因?yàn)楣ぷ髡咧罌](méi)有其他線程可以修改它們的數(shù)據(jù)腺怯,所以工作者是有狀態(tài)的。使用有狀態(tài)這個(gè)單詞洪灯,我的意思是它們可以在內(nèi)部保存它們需要在內(nèi)存中操作的數(shù)據(jù)逃沿,只需將改變寫(xiě)回外存儲(chǔ)系統(tǒng)婴渡。一個(gè)有狀態(tài)的工作者因此比無(wú)狀態(tài)工作者更快幻锁。
3. 更好的硬件適應(yīng)性 Better Hardware Conformity
單線程代碼有著更符合底層硬件工作的優(yōu)勢(shì)。首先边臼,當(dāng)你知道代碼在單線程模式下被執(zhí)行哄尔,你可以創(chuàng)建更多這種情況下最有效的數(shù)據(jù)結(jié)構(gòu)和算法。
其次柠并,單線程有狀態(tài)工作者可以像上面提到的那樣在內(nèi)存中緩存數(shù)據(jù)岭接。當(dāng)數(shù)據(jù)緩存在內(nèi)存中,它也有更大的幾率被緩存到執(zhí)行線程的CPU緩存中臼予,這使得獲得數(shù)據(jù)更加快速鸣戴。
當(dāng)代碼以這種方式編寫(xiě)時(shí),它能夠從底層硬件工作中獲取好處粘拾,所以我將它稱為硬件適應(yīng) (hardware conformity)窄锅。一些開(kāi)發(fā)者稱它為機(jī)械和諧 (mechanical sympathy)。我更喜歡硬件適應(yīng)這個(gè)詞缰雇,因?yàn)橛?jì)算機(jī)只有很少的機(jī)械部件入偷,并且"sympathy"這個(gè)單詞在本文中被使用作為"更好匹配"的象征,我相信"conform"這個(gè)詞可以表達(dá)的更好械哟。不管怎么說(shuō)疏之,這是細(xì)枝末節(jié)的東西。使用哪個(gè)單詞全憑你喜好暇咆。
4. 作業(yè)順序的可能的
使用流水線并發(fā)模型來(lái)保證作業(yè)順序锋爪,并以此來(lái)實(shí)現(xiàn)并發(fā)系統(tǒng)是可能的。作業(yè)順序保證使它更容易去及時(shí)表達(dá)某個(gè)點(diǎn)上的系統(tǒng)狀態(tài)爸业。更進(jìn)一步來(lái)說(shuō)其骄,你可以把將會(huì)到來(lái)的作業(yè)寫(xiě)入日志中。這個(gè)日志可以被使用來(lái)重建系統(tǒng)狀態(tài)沃呢,用來(lái)防止系統(tǒng)的崩潰年栓。作業(yè)以某個(gè)順序?qū)懭氲饺罩局胁鸹樱⑶疫@個(gè)順序成為作業(yè)次序的保證薄霜。
實(shí)現(xiàn)一個(gè)確定的作業(yè)次序不是簡(jiǎn)單的,但是經(jīng)常是必要的纸兔。如果你可以做到的話惰瓜,它能夠簡(jiǎn)化像回滾,重存數(shù)據(jù)汉矿,復(fù)制數(shù)據(jù)等任務(wù)的操作崎坊,這可以通過(guò)日志文件來(lái)完成。
流水線缺點(diǎn)
流水線并發(fā)模型的主要缺點(diǎn)是作業(yè)的執(zhí)行經(jīng)常分布在多個(gè)工作者中洲拇,因此分布在你的項(xiàng)目中的多個(gè)class類中奈揍,這將很難精確的觀察哪部分代碼正在被執(zhí)行曲尸。
它也可能很難去編寫(xiě)代碼。工作者代碼有時(shí)被寫(xiě)成回調(diào)處理男翰,有許多嵌套回調(diào)處理的代碼讓一些開(kāi)發(fā)者稱之為回調(diào)噩夢(mèng) (callback hell)另患。回調(diào)噩夢(mèng)意味著它很難跟蹤什么代碼正在工作蛾绎,以及確保每個(gè)回調(diào)能夠獲取它需要的數(shù)據(jù)昆箕。
此時(shí)使用并行工作者并發(fā)模型往往很簡(jiǎn)單,你可以打開(kāi)工作者代碼文件并且從頭到尾的閱讀代碼租冠。當(dāng)然并行工作者代碼也可能分布在不同的類中鹏倘,但是執(zhí)行序列一般很容易從代碼中閱讀了解。
函數(shù)式并行
函數(shù)式并行是第三種并發(fā)模型顽爹,最近被討論了很多(2015)纤泵。
函數(shù)式并行的基本思想是使用函數(shù)調(diào)用實(shí)現(xiàn)你的程序。函數(shù)可以被視作"代理人"或者"執(zhí)行人"然后彼此發(fā)送信息镜粤,這很像流水線并發(fā)模型(也叫做響應(yīng)式或事件驅(qū)動(dòng)系統(tǒng))夕吻。當(dāng)一個(gè)函數(shù)調(diào)用另一個(gè)時(shí),這與發(fā)送一個(gè)消息相似繁仁。
所有傳給函數(shù)的參數(shù)被拷貝涉馅,所以沒(méi)有接受參數(shù)的函數(shù)之外的實(shí)例可以操縱數(shù)據(jù)。這個(gè)拷貝對(duì)于避免共享數(shù)據(jù)競(jìng)爭(zhēng)條件是必要的黄虱,也使得函數(shù)執(zhí)行和一個(gè)原子操作相似稚矿。每個(gè)函數(shù)調(diào)用可以獨(dú)立于其他函數(shù)調(diào)用被執(zhí)行。
每個(gè)函數(shù)調(diào)用可以被獨(dú)立執(zhí)行捻浦,因此每個(gè)函數(shù)調(diào)用可以在不同的CPU上執(zhí)行晤揣,那意味著一個(gè)為實(shí)用而實(shí)現(xiàn)的算法(an algorithm implemented functionally)可以在多個(gè)CPU上并行執(zhí)行。
在Java 7中我們可以使用java.util.concurrent
包朱灿,包含 ForkAndJoinPool 昧识,可以幫助你實(shí)現(xiàn)類似于函數(shù)式并行的一些行為。在Java 8中我們可以使用并行 streams 盗扒,可以幫助你并行遍歷大的集合跪楞。
函數(shù)式并行困難的部分是知道哪些函數(shù)調(diào)用是并行的。通過(guò)CPU協(xié)調(diào)函數(shù)調(diào)用伴隨著一定的開(kāi)銷侣灶,通過(guò)一個(gè)函數(shù)完成的工作單元需要值得這個(gè)開(kāi)銷才行甸祭。如果函數(shù)很小,嘗試使他們并行可能會(huì)比單線程褥影,單CPU執(zhí)行更慢池户。
從我的理解 (這一點(diǎn)都不完美), 你可以實(shí)現(xiàn)一個(gè)基于響應(yīng)式、事件驅(qū)動(dòng)模型的算法, 并實(shí)現(xiàn)類似于通過(guò)函數(shù)并行實(shí)現(xiàn)的工作分解。有了一個(gè)更加驅(qū)動(dòng)的模型(driven model), 你就能更好地控制要并行什么以及多少(在我看來(lái))校焦。
此外赊抖,將任務(wù)拆在多個(gè) CPU上引發(fā)的協(xié)調(diào)的開(kāi)銷,只有在該任務(wù)當(dāng)前是程序正在執(zhí)行的唯一任務(wù)時(shí)才有意義寨典。但是熏迹, 如果系統(tǒng)同時(shí)執(zhí)行多個(gè)其他任務(wù) (例如 web 服務(wù)器、數(shù)據(jù)庫(kù)服務(wù)器和許多其他系統(tǒng))凝赛,則嘗試并行化單個(gè)任務(wù)是沒(méi)有意義的注暗。無(wú)論如何,計(jì)算機(jī)中的其他CPU都會(huì)忙于處理其他任務(wù)墓猎,因此沒(méi)有理由嘗試用較慢的捆昏、函數(shù)式并行的任務(wù)來(lái)干擾它們。使用流水線 (響應(yīng)式) 并發(fā)模型可能會(huì)更好毙沾, 因?yàn)樗拈_(kāi)銷較小 (在單線程模式下按順序執(zhí)行)骗卜, 并且更符合底層硬件的工作方式。
哪個(gè)并發(fā)模型最好左胞?
所以寇仓,哪個(gè)并發(fā)模型更好呢?
像往常一樣烤宙,這個(gè)答案依賴于你的系統(tǒng)想要做什么遍烦。如果你的工作是并行的,獨(dú)立的并且沒(méi)有共享狀態(tài)的必要躺枕,你可以使用并行工作者模型來(lái)實(shí)現(xiàn)服猪。
許多工作不是并行的和獨(dú)立的,對(duì)于這種類型的系統(tǒng)我相信流水線并發(fā)模型會(huì)有更多的好處拐云,并且比起并行工作者模型來(lái)說(shuō)也更好罢猪。
你甚至不需要自己編寫(xiě)流水線并發(fā)模型底層,現(xiàn)代平臺(tái)例如 Vert.x 已經(jīng)為你實(shí)現(xiàn)了許多叉瘩。
同一線程 Same-threading
同一線程是一個(gè)單線程系統(tǒng)被拓展為N個(gè)單線程系統(tǒng)的的并發(fā)模型膳帕。最終結(jié)果是N個(gè)單線程系統(tǒng)并行運(yùn)行。
同一線程系統(tǒng)不是純粹的單線程系統(tǒng)薇缅,因?yàn)樗鄠€(gè)線程危彩。但是 —— 每個(gè)線程都像單線程系統(tǒng)一樣運(yùn)行。
為什么使用單線程系統(tǒng)?
您可能想知道為什么有人會(huì)在今天設(shè)計(jì)單線程系統(tǒng)捅暴。單線程系統(tǒng)已經(jīng)普及恬砂,因?yàn)樗鼈兊牟l(fā)模型比多線程系統(tǒng)簡(jiǎn)單得多咧纠。單線程系統(tǒng)不與其他線程共享任何數(shù)據(jù)蓬痒。這使單線程能夠使用非并發(fā)數(shù)據(jù)結(jié)構(gòu),并且可以更好地利用CPU和CPU緩存漆羔。
但不幸的是梧奢,單線程系統(tǒng)無(wú)法充分利用現(xiàn)代CPU∮啵現(xiàn)代CPU通常配有2個(gè)、4個(gè)或更多核心亲轨。每個(gè)核心都可以作為單獨(dú)的CPU運(yùn)行趋惨。單線程系統(tǒng)只能使用其中一個(gè)內(nèi)核,如下所示:
同一線程惦蚊,單線程擴(kuò)展
為了利用CPU中的所有內(nèi)核器虾,可以擴(kuò)展單線程系統(tǒng)以利用整個(gè)計(jì)算機(jī)。
每個(gè)CPU一個(gè)線程
同一線程系統(tǒng)通常在計(jì)算機(jī)中每個(gè)CPU運(yùn)行1個(gè)線程蹦锋。如果計(jì)算機(jī)包含4個(gè)CPU或CPU具有4個(gè)內(nèi)核兆沙,則運(yùn)行同一線程系統(tǒng)的4個(gè)實(shí)例(4個(gè)單線程系統(tǒng))是很正常的。下圖顯示了這一原則:
不共享狀態(tài)
同一線程系統(tǒng)看起來(lái)類似于多線程系統(tǒng)莉掂,因?yàn)橥痪€程系統(tǒng)也運(yùn)行多個(gè)線程葛圃,但是有一個(gè)微妙的區(qū)別。
同一線程和多線程系統(tǒng)之間的區(qū)別在于同一線程系統(tǒng)中的線程不共享狀態(tài)憎妙。線程并不并發(fā)訪問(wèn)共享內(nèi)存库正,也沒(méi)有線程共享數(shù)據(jù)的并發(fā)數(shù)據(jù)結(jié)構(gòu)等。這種差異在這里說(shuō)明:
不共享狀態(tài)使每個(gè)線程表現(xiàn)的像是單線程系統(tǒng)厘唾。但是褥符,由于同一線程系統(tǒng)可以包含多個(gè)線程,因此它實(shí)際上不是“單線程系統(tǒng)”抚垃。因?yàn)槿狈Ω玫拿Q属瓣,我發(fā)現(xiàn)將這樣的系統(tǒng)稱為同一線程系統(tǒng)更精確,而不是“具有單線程設(shè)計(jì)的多線程系統(tǒng)”讯柔。同一線程更容易表達(dá),更容易理解粗截。
同一線程基本上意味著數(shù)據(jù)處理一直在同一個(gè)線程中捣炬,并且同一線程系統(tǒng)中的任何線程不會(huì)并發(fā)使用共享數(shù)據(jù)熊昌。
負(fù)載分配
顯然湿酸,同一線程系統(tǒng)需要在運(yùn)行的單線程實(shí)例之間共享工作負(fù)載。如果不這樣推溃,只有一個(gè)實(shí)例可以得到所有工作昂利,系統(tǒng)實(shí)際上是單線程的。
具體如何在不同實(shí)例上分配負(fù)載取決于系統(tǒng)的設(shè)計(jì)蜂奸,我將在以下部分介紹幾個(gè)扩所。
1.單線程微服務(wù)
如果您的系統(tǒng)由多個(gè)微服務(wù)組成,則每個(gè)微服務(wù)都可以在單線程模式下運(yùn)行助赞。當(dāng)您將多個(gè)單線程微服務(wù)部署到同一臺(tái)機(jī)器時(shí)袁勺,每個(gè)微服務(wù)都可以在一個(gè)CPU上啟動(dòng)一個(gè)線程運(yùn)行魁兼。
微服務(wù)本質(zhì)上不共享任何數(shù)據(jù),因此微服務(wù)是同一線程系統(tǒng)的一個(gè)很好的用例盖呼。
2.具有分片數(shù)據(jù)的服務(wù)
如果您的系統(tǒng)確實(shí)需要共享數(shù)據(jù)化撕,或者至少需要共享數(shù)據(jù)庫(kù)植阴,則可以對(duì)數(shù)據(jù)庫(kù)進(jìn)行分片。分片意味著數(shù)據(jù)在多個(gè)數(shù)據(jù)庫(kù)之間分配憾朴。通常對(duì)數(shù)據(jù)進(jìn)行劃分,使得彼此相關(guān)的所有數(shù)據(jù)一起位于同一數(shù)據(jù)庫(kù)中喷鸽。例如众雷,屬于某個(gè)“owner”實(shí)體的所有數(shù)據(jù)都插入到同一數(shù)據(jù)庫(kù)中脐湾。但是垂攘,分片不在本教程的范圍內(nèi)也搓,因此你有興趣的話狠鸳,需要自己搜索有關(guān)該主題的教程。
線程通信
如果同一線程系統(tǒng)中的線程需要通信舒岸,則它們通過(guò)消息傳遞來(lái)實(shí)現(xiàn)。想要向線程A發(fā)送消息的線程可以通過(guò)生成消息(字節(jié)序列)來(lái)實(shí)現(xiàn)俄认。然后眯杏,線程B可以復(fù)制該消息(字節(jié)序列)并讀取它岂贩。通過(guò)復(fù)制消息,線程B可以確保線程A在自己讀取時(shí)不會(huì)修改消息卸伞。復(fù)制后荤傲,它對(duì)于線程A來(lái)說(shuō)是不可變的颈渊。
通過(guò)消息傳遞的線程通信如下所示:
線程通信可以通過(guò)隊(duì)列俊嗽,管道绍豁,UNIX套接字妹田,TCP套接字等實(shí)現(xiàn),具體看哪個(gè)適合您的系統(tǒng)驶拱。
更簡(jiǎn)單的并發(fā)模型
在同一線程系統(tǒng)中蓝纲,它自己的線程中運(yùn)行的每個(gè)系統(tǒng)都可以像單線程一樣實(shí)現(xiàn)。這意味著它的內(nèi)部并發(fā)模型會(huì)變得比線程共享狀態(tài)簡(jiǎn)單得多永丝,您不必?fù)?dān)心并發(fā)數(shù)據(jù)結(jié)構(gòu)以及此類數(shù)據(jù)結(jié)構(gòu)可能導(dǎo)致的所有并發(fā)問(wèn)題慕嚷。
插圖
以下是單線程喝检,多線程和同一線程系統(tǒng)的說(shuō)明撼泛,因此您可以更輕松地了解它們之間的差異愿题。
第一個(gè)插圖展現(xiàn)了單線程系統(tǒng)。
第二個(gè)圖展現(xiàn)了一個(gè)多線程系統(tǒng),其中線程共享數(shù)據(jù)拧咳。
第三個(gè)圖展現(xiàn)了一個(gè)有2個(gè)線程且每個(gè)線程具有自己數(shù)據(jù)的同一線程系統(tǒng)囚灼,通過(guò)將消息相互傳遞進(jìn)行通信灶体。
并發(fā) vs. 并行
術(shù)語(yǔ)并發(fā) 和并行 通常用于多線程程序蝎抽。但是并發(fā)和并行究竟意味著什么樟结,它們是相同的術(shù)語(yǔ)還是什么?
最簡(jiǎn)潔的答案是“不是”碎连。它們不是相同的術(shù)語(yǔ)鱼辙,盡管它們?cè)诒砻嫔峡雌饋?lái)非常相似。我花了一些時(shí)間才最終找到并理解并發(fā)和并行之間的區(qū)別倒戏。因此杜跷,我決定在這個(gè)Java并發(fā)教程中添加這個(gè)關(guān)于并發(fā)和并行的章節(jié)葱椭。
并發(fā)
并發(fā)意味著應(yīng)用程序有多個(gè)任務(wù)同時(shí)進(jìn)行(并發(fā))口四。如果計(jì)算機(jī)只有一個(gè)CPU蔓彩,則應(yīng)用程序無(wú)法同一時(shí)間 在多個(gè)任務(wù)上取得進(jìn)展 赤嚼,但是在應(yīng)用程序內(nèi)部有多個(gè)任務(wù)正在執(zhí)行更卒。在下一個(gè)任務(wù)開(kāi)始之前,它并沒(méi)有完全完成任務(wù)俯萌。
并行
并行意味著應(yīng)用程序?qū)⑵淙蝿?wù)分成較小的子任務(wù)咐熙,這些子任務(wù)可以并行處理棋恼,例如在同一時(shí)間在多個(gè)CPU上锈玉。
Concurrency和Parallelism在細(xì)節(jié)上的區(qū)別
如你所見(jiàn)拉背,并發(fā)與應(yīng)用程序處理其工作的多個(gè)任務(wù)有關(guān)去团。應(yīng)用程序可以在某個(gè)時(shí)間(順序)處理一個(gè)任務(wù),或者同時(shí)處理多個(gè)任務(wù)(同時(shí))肴熏。
另一方面顷窒,并??行與應(yīng)用程序處理每個(gè)單獨(dú)任務(wù)的方式有關(guān)鞋吉。應(yīng)用程序可以從開(kāi)始到結(jié)束連續(xù)地處理任務(wù)谓着,或者將任務(wù)分成可以并行完成的子任務(wù)。
如您所見(jiàn)治筒,應(yīng)用程序可以是并發(fā)的耸袜,但不是并行的堤框。這意味著它同時(shí)處理多個(gè)任務(wù)纵柿,但任務(wù)不會(huì)分解為子任務(wù)藐窄。
應(yīng)用程序也可以是并行的但不是并發(fā)的荆忍。這意味著應(yīng)用程序一次只能處理一個(gè)任務(wù),并且此任務(wù)被分解為可以并行處理的子任務(wù)叽唱。
此外棺亭,應(yīng)用程序可以是既不并發(fā)也不并行的镶摘。這意味著它一次只能處理一個(gè)任務(wù)凄敢,并且任務(wù)永遠(yuǎn)不會(huì)分解為并行執(zhí)行的子任務(wù)。
最后扑庞,應(yīng)用程序也可以是既并發(fā)又并行的罐氨,因?yàn)樗瓤梢酝瑫r(shí)處理多個(gè)任務(wù)栅隐,也可以將每個(gè)任務(wù)分解為子任務(wù)以便并行執(zhí)行狠怨。但是佣赖,在這種情況下憎蛤,并發(fā)和并行的一些好處可能會(huì)丟失纪吮,因?yàn)橛?jì)算機(jī)中的CPU已經(jīng)相當(dāng)忙于單獨(dú)的并發(fā)或并行碾盟。將它組合在一起可能只會(huì)帶來(lái)很小的性能提升甚至性能損失冰肴。在盲目采用并發(fā)并行模型之前熙尉,請(qǐng)確保進(jìn)行分析和測(cè)量检痰。
以下為原網(wǎng)站: