學(xué)識(shí)甚淺,大家僅作參考吧。
對(duì)于初學(xué)者來書,這一章涉及到的知識(shí)點(diǎn)很多,在這之前,我總結(jié)幾點(diǎn)重要的知識(shí)點(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)容做一個(gè)假設(shè),即每個(gè)線程運(yùn)行在自己的處理器上,不考慮基于時(shí)間片分時(shí)實(shí)現(xiàn)的多線程,即我們這章討論的多線程是指不同的處理器上的線程。(因?yàn)榛跁r(shí)間片分時(shí)的多線程也就是單一處理器的多線程在有序性上討論起來很麻煩,大家可以參考Java多線程編程指南,也可以留言)
前兩點(diǎn)基礎(chǔ)大家上網(wǎng)查查,我主要講一下導(dǎo)致并發(fā)問題的第三點(diǎn)。先明白幾個(gè)概念:
源代碼順序:源代碼中所指定的內(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ǔ)上我們將重排序分為兩部分:指令重排序與存儲(chǔ)子系統(tǒng)重排序。
指令重排序:表現(xiàn)在 程序順序與源代碼順序不一致 或者 執(zhí)行順序與程序順序 不一致。
解釋一下就是:源代碼中指定的內(nèi)存訪問順序與得到的字節(jié)碼順序不一樣 或者 字節(jié)碼順序與實(shí)際的執(zhí)行順序不一樣。
既然產(chǎn)生了不一樣,那么問題肯定是出在連接這三個(gè)過程的中間件上面。學(xué)過java的同學(xué)應(yīng)該知道,java平臺(tái)包括兩種編譯器:
靜態(tài)編譯器(javac)和動(dòng)態(tài)編譯器(jit:just in time)。靜態(tài)編譯器是將.java文件編譯成.class文件(二進(jìn)制文件),之后便可以解釋執(zhí)行。動(dòng)態(tài)編譯器是將.class文件編譯成機(jī)器碼,之后再由jvm運(yùn)行。jit主要是做性能上面的優(yōu)化,如熱點(diǎn)代碼編譯成本地代碼,加速調(diào)用。(說點(diǎn)題外話,這些東西本來應(yīng)該在課上就應(yīng)該學(xué)到的,但是......誒)好的,那么指令重排序的根源主要在哪呢?
其實(shí)javac基本不會(huì)調(diào)整指令順序,調(diào)整指令順序的大多出在jit優(yōu)化上。
有沒有人想問,既然jit優(yōu)化會(huì)出問題,那么為什么還要這個(gè)優(yōu)化?。?!(我覺得能問出問題起碼跟上了)
在單線程情況下,我們并不在乎具體的內(nèi)存訪問順序是什么樣的,只要展示出來的結(jié)果是按照我的源代碼順序執(zhí)行的就好了,我不會(huì)管你究竟在我的字節(jié)碼或者機(jī)器碼中調(diào)整了怎樣的順序。 也就是說,編譯器的優(yōu)化它并不會(huì)造成結(jié)果的偏差,但是帶來的性能的提升確實(shí)巨大的,就好像你的mysql用了索引和沒用索引一樣,代碼量上去之后,優(yōu)化就是必須的。
所以,問題就出在了并發(fā)訪問時(shí),你一旦調(diào)整了指令順序,而且又在沒有同步的情況下,那么我的一個(gè)線程就很可能讀到另一個(gè)線程操作的中間過程。給大家舉個(gè)例子(第一章提到的初始化問題):
Person p = new Person();那么我們的Jit編譯器會(huì)怎樣操作呢?會(huì)分為以下三個(gè)子操作,
①.分配Person實(shí)例所需要的內(nèi)存空間;
objRef = allocate(Person.class);(推薦大家看一下Java反射機(jī)制,很重要的很基礎(chǔ)的很...有用的=@=)
②.調(diào)用Person的構(gòu)造方法初始化objRef引用指向一個(gè)Person實(shí)例;
invokeConstructor(objRef);
③.將Person實(shí)例引用objRef賦值給實(shí)例變量p;
p = objRef;
在優(yōu)化的時(shí)候,我們很可能將操作③調(diào)整到操作②之前進(jìn)行,也就是先將一個(gè)空的實(shí)例賦給p。那么多線程訪問的時(shí)候,其它線程很可能用這個(gè)空的實(shí)例,從而造成錯(cuò)誤。
存儲(chǔ)子系統(tǒng)重排序:表現(xiàn)在感知順序與執(zhí)行順序不一樣。
首先我們要明確一下什么是存儲(chǔ)子系統(tǒng):簡(jiǎn)單理解就是主存與寄存器之間的高速緩存,細(xì)一點(diǎn)的話可以加上寫緩沖器(提高寫主存的效率)。
假設(shè)我們兩個(gè)內(nèi)存訪問操作都是嚴(yán)格按照程序順序執(zhí)行的,即不發(fā)生指令重排的情況,在存儲(chǔ)子系統(tǒng)的作用下也會(huì)造成其他處理器(線程) 感知到 這兩個(gè)內(nèi)存訪問操作的順序不一樣。那么,這兩個(gè)操作可以有四種:其實(shí)就是讀操作和寫操作的排列組合。
以讀寫操作為例:在重排序的作用下,會(huì)讓其他線程感覺讀操作被排到了寫操作之后。
但是可能還是不太清楚,考慮兩個(gè)線程P1和P2,它們有兩個(gè)共享變量data(int)和ready(boolean),P1的任務(wù)是更新data并將ready變?yōu)閠rue,P2的任務(wù)是不斷輪詢r(jià)eady的值,當(dāng)ready為true時(shí)打印出data的值?,F(xiàn)在P1更新了data,并將ready置為true,并在無指令重排的情況下把值都放到寫緩沖區(qū)。但是,寫緩沖區(qū)并不保證操作的先入先出原則,即可能先把ready的值更新回高速緩存(或主存),然后再把data值寫回。那么在兩個(gè)操作之間,P2可能看見了ready為true,而此時(shí)data的新值還在寫緩存中,并未更新回去,就造成了錯(cuò)誤。
東西其實(shí)還蠻多的,大家細(xì)細(xì)體會(huì),下一章我們?cè)倬唧w討論有序性。