并發(fā)編程
最近在學(xué)習(xí)即刻時(shí)間王寶令老師的并發(fā)編程專欄,有一種醍醐灌頂?shù)母杏X(jué)。故對(duì)前四篇文章做個(gè)總結(jié)。
如何設(shè)計(jì)一個(gè)并發(fā)程序?
把并發(fā)編程抽象一層,你會(huì)發(fā)現(xiàn)當(dāng)你寫(xiě)一個(gè)并發(fā)程序的時(shí)候,干的就是三件事:
分工:考慮的事把一個(gè)任務(wù)拆解成多少個(gè)子任務(wù)分別分配給多少個(gè)線程做呢,比如說(shuō)采用并發(fā)的方式實(shí)現(xiàn)wordcount,那么我可能能把這個(gè)任務(wù)拆解成 1.對(duì)文件進(jìn)行切割 2.對(duì)這些不同的切割分別計(jì)算它們的所包含的詞的數(shù)量 3.把最后的結(jié)果輸出出來(lái)。
而這三類任務(wù),1和3在主線程中做,2就由多個(gè)線程進(jìn)行并行執(zhí)行。
協(xié)作: 考慮的是任務(wù)啟動(dòng)的時(shí)機(jī),最主要的就是任務(wù)之間的依賴關(guān)系,如同一個(gè)DAG一般,一個(gè)線程執(zhí)行完了一個(gè)任務(wù),要通知哪些后續(xù)的任務(wù)的線程開(kāi)工。而對(duì)于上面的例子而言,主線程一邊做著1任務(wù),切割了一個(gè)就啟動(dòng)一個(gè)線程干2任務(wù),然后等所有的2任務(wù)干完了,主線程就可以干三任務(wù)。
線程安全(互斥):分工同步考慮了程序的性能,但并發(fā)程序中準(zhǔn)確性問(wèn)題同樣至關(guān)重要。從任務(wù)具體執(zhí)行的角度上看,任務(wù)之間同樣可能不是說(shuō)能夠獨(dú)立運(yùn)行,因?yàn)樗麄兛赡芄蚕碇兞?,可能?huì)有變量的可見(jiàn)性,原子性,順序性的問(wèn)題。接著上面的例子,我們?cè)谌蝿?wù)2中用了一個(gè)共享變量count計(jì)算詞的總數(shù),由于多個(gè)線程對(duì)count進(jìn)行操作,所以可能就需要對(duì)這個(gè)count采用原子的方式進(jìn)行修改,比如用鎖,用CAS等等。

(我其實(shí)覺(jué)得協(xié)作互斥的名字起得不是很好,但我也沒(méi)有想到能夠簡(jiǎn)單表達(dá)意思的好名字,??。)
并發(fā)編程的問(wèn)題根源是啥子?
計(jì)算機(jī)在飛速發(fā)展,但核心矛盾仍然是CPU,內(nèi)存,磁盤的性能差距,形象點(diǎn)說(shuō),CPU 是天上一天,內(nèi)存是地上一年;而如果內(nèi)存是天上一天,I/O 設(shè)備是地上十年。于實(shí)乎就有什么L1 L2等告訴緩存去均衡CPU與內(nèi)存的速度差距;就有對(duì)進(jìn)程,線程對(duì)CPU進(jìn)行分時(shí)復(fù)用,均衡CPU與I/O設(shè)備的差距;編譯程序優(yōu)化指令執(zhí)行次序,使得緩存能夠得到更加合理地利用。而這三方面的改進(jìn),同時(shí)也引入了三方面的問(wèn)題,緩存的存在,數(shù)據(jù)的修改如何讓其他線程看見(jiàn),可見(jiàn)性問(wèn)題;比如一個(gè)I++的操作,多個(gè)線程運(yùn)行,線程切換的時(shí)候,就可能導(dǎo)致這個(gè)i++操作停i變量的加載過(guò)程中,從而導(dǎo)致原子性問(wèn)題。指令順序的重排,導(dǎo)致其他線程不能以該線程的代碼順序做假定編寫(xiě)代碼。比如下面事例,線程二就不能因?yàn)榫€程一中i=10 是在isOutPut = true之前的,所以就認(rèn)為輸出的i一定是10。
//線程一
i = 10
isOutput = true;
//線程二
if(isOutput){
System.out.println(i);
}
java內(nèi)存模型,能不能講的對(duì)程序員友好點(diǎn)?
什么java內(nèi)存模型,尤其是happens-before規(guī)則,一直以來(lái)我就感覺(jué)要么是廢話,要么不知道腫么用。王寶令老師給了一個(gè)特別對(duì)廣大程序員友好的解釋。因?yàn)榫彺鎸?dǎo)致了可見(jiàn)性問(wèn)題,因?yàn)橹噶钪嘏艑?dǎo)致了順序性問(wèn)題,那么我們是不是可以通過(guò)按需禁止緩存,按需禁止指令重排解決進(jìn)行解決呢。而java內(nèi)存模型正是規(guī)范了按需禁止緩存指令重排的方法。這些方法包括 volatile、synchronized 和final 以及happenbefore的六項(xiàng)規(guī)則。
happenbefore 影響的是順序性,和可見(jiàn)性語(yǔ)義
- 在同一個(gè)線程中,前面的操作happens-before于后面操作。這個(gè)規(guī)則其實(shí)很顯然,但需要強(qiáng)調(diào)的是這并不是說(shuō)禁止指令重排。在i=10的后面,你都可以利用i=10這個(gè)信息,但如果你沒(méi)用,是不是說(shuō)我放到j(luò)=10后面執(zhí)行也ok呢。
i=10;
j=10;
- volatile 變量規(guī)則 , 對(duì)volatile變量的寫(xiě)操作 happens-before 對(duì)volatile變量的讀操作。這條規(guī)則好像和禁用緩存的效果是一致的, 所以對(duì)這條規(guī)則我一直有個(gè)問(wèn)題,就是首先這條規(guī)則無(wú)疑是針對(duì)一個(gè)線程寫(xiě),另外的線程讀的場(chǎng)景的,那么我在寫(xiě)線程二的程序的時(shí)候,同樣還是有順序性問(wèn)題呀?
//線程一
i = 10 //操作1
volatile isOutput = true;//操作2
//線程二
if(isOutput){//操作3
System.out.println(i);//操作4
}
然而我忽略了
- 傳遞性規(guī)則如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
有什么用呢? 如果線程二if isOutput 為true了,那由規(guī)則2 操作2 happens-before 操作3,由規(guī)則一 操作3 happens-before 操作4,所以操作2 happens-before 操作4,同理可證 操作1happens-before 操作4。 簡(jiǎn)直棒呆。所以1,2,3規(guī)則結(jié)合起來(lái)用,也就說(shuō)明了volatile修飾變量的禁用緩存,禁用指令重排的語(yǔ)義。
管程中的鎖規(guī)則
對(duì)一個(gè)鎖的解鎖 Happens-Before 于后續(xù)對(duì)這個(gè)鎖的加鎖操作。線程start() 規(guī)則
它是指主線程 A 啟動(dòng)子線程 B 后,子線程 B 能夠看到主線程在啟動(dòng)子線程 B 前的操作。線程join()規(guī)則
主線程A等待子線程B運(yùn)行完后,主線程A能夠看到子線程B的操作。