學(xué)識甚淺盟迟,大家僅作參考吧。
對于初學(xué)者來書潦闲,這一章涉及到的知識點(diǎn)很多攒菠,在這之前,我總結(jié)幾點(diǎn)重要的知識點(diǎn):
1.Java內(nèi)存模型歉闰。為什么內(nèi)存模型這么重要辖众?其實(shí)細(xì)想一下,多線程和單線程相比和敬,出現(xiàn)問題不就是內(nèi)存里面的值可能與預(yù)期值(單線程運(yùn)行/串行運(yùn)行)之間不一致嘛凹炸。所以一定至少要知道讀寫操作是怎么操作內(nèi)存的!昼弟!
2.Java程序的運(yùn)行順序啤它。程序是如何按照happen-before原則運(yùn)行的。
3.Java重排序。
4.本章內(nèi)容做一個假設(shè)变骡,即每個線程運(yùn)行在自己的處理器上救欧,不考慮基于時(shí)間片分時(shí)實(shí)現(xiàn)的多線程,即我們這章討論的多線程是指不同的處理器上的線程锣光。(因?yàn)榛跁r(shí)間片分時(shí)的多線程也就是單一處理器的多線程在有序性上討論起來很麻煩,大家可以參考Java多線程編程指南铝耻,也可以留言)
前兩點(diǎn)基礎(chǔ)大家上網(wǎng)查查誊爹,我主要講一下導(dǎo)致并發(fā)問題的第三點(diǎn)。先明白幾個概念:
源代碼順序:源代碼中所指定的內(nèi)存訪問操作順序瓢捉。
程序順序:我們可以理解為編譯得到機(jī)器碼或者解釋執(zhí)行的字節(jié)碼(之后把兩者統(tǒng)稱為字節(jié)碼)所指定的內(nèi)存訪問順序频丘。
執(zhí)行順序:內(nèi)存訪問在指定處理器上的實(shí)際執(zhí)行順序。
感知順序:給定處理器感知到其他處理器內(nèi)存訪問的順序泡态。
有點(diǎn)難度的東西來了搂漠,看不下去就一點(diǎn)一點(diǎn)看吧=o=
在此基礎(chǔ)上我們將重排序分為兩部分:指令重排序與存儲子系統(tǒng)重排序。
指令重排序:表現(xiàn)在 程序順序與源代碼順序不一致 或者 執(zhí)行順序與程序順序 不一致某弦。
解釋一下就是:源代碼中指定的內(nèi)存訪問順序與得到的字節(jié)碼順序不一樣 或者 字節(jié)碼順序與實(shí)際的執(zhí)行順序不一樣桐汤。
既然產(chǎn)生了不一樣,那么問題肯定是出在連接這三個過程的中間件上面靶壮。學(xué)過java的同學(xué)應(yīng)該知道怔毛,java平臺包括兩種編譯器:
靜態(tài)編譯器(javac)和動態(tài)編譯器(jit:just in time)。靜態(tài)編譯器是將.java文件編譯成.class文件(二進(jìn)制文件)腾降,之后便可以解釋執(zhí)行拣度。動態(tài)編譯器是將.class文件編譯成機(jī)器碼,之后再由jvm運(yùn)行螃壤。jit主要是做性能上面的優(yōu)化抗果,如熱點(diǎn)代碼編譯成本地代碼,加速調(diào)用奸晴。(說點(diǎn)題外話冤馏,這些東西本來應(yīng)該在課上就應(yīng)該學(xué)到的,但是......誒)好的寄啼,那么指令重排序的根源主要在哪呢宿接?
其實(shí)javac基本不會調(diào)整指令順序,調(diào)整指令順序的大多出在jit優(yōu)化上辕录。
有沒有人想問睦霎,既然jit優(yōu)化會出問題,那么為什么還要這個優(yōu)化白叩8迸!(我覺得能問出問題起碼跟上了)
在單線程情況下蚣旱,我們并不在乎具體的內(nèi)存訪問順序是什么樣的碑幅,只要展示出來的結(jié)果是按照我的源代碼順序執(zhí)行的就好了戴陡,我不會管你究竟在我的字節(jié)碼或者機(jī)器碼中調(diào)整了怎樣的順序。 也就是說沟涨,編譯器的優(yōu)化它并不會造成結(jié)果的偏差恤批,但是帶來的性能的提升確實(shí)巨大的,就好像你的mysql用了索引和沒用索引一樣裹赴,代碼量上去之后喜庞,優(yōu)化就是必須的。
所以棋返,問題就出在了并發(fā)訪問時(shí)延都,你一旦調(diào)整了指令順序,而且又在沒有同步的情況下睛竣,那么我的一個線程就很可能讀到另一個線程操作的中間過程晰房。給大家舉個例子(第一章提到的初始化問題):
Person p = new Person();那么我們的Jit編譯器會怎樣操作呢?會分為以下三個子操作射沟,
①.分配Person實(shí)例所需要的內(nèi)存空間;
objRef = allocate(Person.class);(推薦大家看一下Java反射機(jī)制殊者,很重要的很基礎(chǔ)的很...有用的=@=)
②.調(diào)用Person的構(gòu)造方法初始化objRef引用指向一個Person實(shí)例;
invokeConstructor(objRef);
③.將Person實(shí)例引用objRef賦值給實(shí)例變量p;
p = objRef;
在優(yōu)化的時(shí)候验夯,我們很可能將操作③調(diào)整到操作②之前進(jìn)行幽污,也就是先將一個空的實(shí)例賦給p。那么多線程訪問的時(shí)候簿姨,其它線程很可能用這個空的實(shí)例距误,從而造成錯誤。
存儲子系統(tǒng)重排序:表現(xiàn)在感知順序與執(zhí)行順序不一樣扁位。
首先我們要明確一下什么是存儲子系統(tǒng):簡單理解就是主存與寄存器之間的高速緩存准潭,細(xì)一點(diǎn)的話可以加上寫緩沖器(提高寫主存的效率)。
假設(shè)我們兩個內(nèi)存訪問操作都是嚴(yán)格按照程序順序執(zhí)行的域仇,即不發(fā)生指令重排的情況刑然,在存儲子系統(tǒng)的作用下也會造成其他處理器(線程) 感知到 這兩個內(nèi)存訪問操作的順序不一樣。那么暇务,這兩個操作可以有四種:其實(shí)就是讀操作和寫操作的排列組合泼掠。
以讀寫操作為例:在重排序的作用下,會讓其他線程感覺讀操作被排到了寫操作之后垦细。
但是可能還是不太清楚择镇,考慮兩個線程P1和P2,它們有兩個共享變量data(int)和ready(boolean)括改,P1的任務(wù)是更新data并將ready變?yōu)閠rue腻豌,P2的任務(wù)是不斷輪詢r(jià)eady的值,當(dāng)ready為true時(shí)打印出data的值。現(xiàn)在P1更新了data吝梅,并將ready置為true虱疏,并在無指令重排的情況下把值都放到寫緩沖區(qū)。但是苏携,寫緩沖區(qū)并不保證操作的先入先出原則做瞪,即可能先把ready的值更新回高速緩存(或主存),然后再把data值寫回右冻。那么在兩個操作之間装蓬,P2可能看見了ready為true,而此時(shí)data的新值還在寫緩存中国旷,并未更新回去,就造成了錯誤茫死。
東西其實(shí)還蠻多的跪但,大家細(xì)細(xì)體會,下一章我們再具體討論有序性峦萎。