并發(fā)本來(lái)就是個(gè)有意思的問(wèn)題,尤其是現(xiàn)在又流行這么一句話:“高帥富加機(jī)器,窮矮搓搞優(yōu)化”。從這句話可以看到,無(wú)論是高帥富還是窮矮搓都需要深入理解并發(fā)編程,高帥富加多了機(jī)器,需要協(xié)調(diào)多臺(tái)機(jī)器或者多個(gè)CPU對(duì)共享資源的訪問(wèn),因此需要了解并發(fā),窮矮搓搞優(yōu)化需要編寫(xiě)各種多線程的代碼來(lái)壓榨 CPU的計(jì)算資源,讓它在同一時(shí)刻做更多的事情,這個(gè)更需要了解并發(fā)。
在我前一篇關(guān)于并發(fā)的文章http://my.oschina.NET/chihz/blog/54731中提到過(guò)管程,管程的特色是在編程語(yǔ)言中 對(duì)并發(fā)的細(xì)節(jié)進(jìn)行封裝,使程序員可以直接在語(yǔ)言中就得到并發(fā)的支持,而不必自己去處理一些像是控制信號(hào)量之類容易出錯(cuò)且繁瑣的細(xì)節(jié)問(wèn)題。一些語(yǔ)言是通過(guò)在 編譯時(shí)解開(kāi)語(yǔ)法糖的方式去實(shí)現(xiàn)管程,但java在編譯后生成的字節(jié)碼層面上對(duì)并發(fā)仍然是一層封裝,比如syncrhonized塊在編譯之后只是對(duì)應(yīng)了兩 條指令:monitorenter和monitorexit。更多的并發(fā)細(xì)節(jié)是在JVM運(yùn)行時(shí)去處理的,而不是編譯。這篇文章主要是針對(duì)JVM處理并發(fā)的 一些細(xì)節(jié)的探討。
JAVA內(nèi)存模型
對(duì)于我們平時(shí)開(kāi)發(fā)的業(yè)務(wù)應(yīng)用來(lái)說(shuō),內(nèi)存應(yīng)該是訪問(wèn)速度最快的存儲(chǔ)設(shè)備,對(duì)于頻繁訪問(wèn)的數(shù)據(jù),我們總是習(xí)慣把它們放到內(nèi)存緩存中,有句話不是說(shuō)么,緩 存就像是清涼油,哪里有問(wèn)題就抹一抹。但是CPU的運(yùn)算速度比起內(nèi)存的訪問(wèn)速度還要快幾個(gè)量級(jí),為了平衡這個(gè)差距,于是就專門(mén)為CPU引入了高速緩存,頻 繁使用的數(shù)據(jù)放到高速緩存當(dāng)中,CPU在使用這些數(shù)據(jù)進(jìn)行運(yùn)算的時(shí)候就不必再去訪問(wèn)內(nèi)存。但是在多CPU時(shí)代卻有一個(gè)問(wèn)題,每個(gè)CPU都擁有自己的高速緩 存,內(nèi)存又是所有CPU共享的公共資源,于是內(nèi)存此時(shí)就成了一個(gè)臨界區(qū),如果控制不好各個(gè)CPU對(duì)內(nèi)存的并發(fā)訪問(wèn),那么就會(huì)產(chǎn)生錯(cuò)誤,出現(xiàn)數(shù)據(jù)不一致的情 況。為了避免這種情況,需要采取緩存一致性協(xié)議來(lái)保證,這類協(xié)議有很多,各個(gè)硬件平臺(tái)和操作系統(tǒng)的實(shí)現(xiàn)不盡相同。
JVM需要實(shí)現(xiàn)跨平臺(tái)的支持,它需要有一套自己的同步協(xié)議來(lái)屏蔽掉各種底層硬件和操作系統(tǒng)的不同,因此就引入了Java內(nèi)存模型。對(duì)于Java來(lái)說(shuō) 開(kāi)發(fā)者并不需要關(guān)心任何硬件細(xì)節(jié),因此沒(méi)有多核CPU和高速緩存的概念,多核CPU和高速緩存在JVM中對(duì)應(yīng)的是Java語(yǔ)言內(nèi)置的線程和每個(gè)線程所擁有 的獨(dú)立內(nèi)存空間,Java內(nèi)存模型所規(guī)范的也就是數(shù)據(jù)在線程自己的獨(dú)立內(nèi)存空間和JVM共享內(nèi)存之間同步的問(wèn)題。下面這兩張圖說(shuō)明了硬件平臺(tái)和JVM內(nèi)存 模型的相似和差異之處。
Java內(nèi)存模型規(guī)定,對(duì)于多個(gè)線程共享的變量,存儲(chǔ)在主內(nèi)存當(dāng)中,每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,線程只能訪問(wèn)自己的工作內(nèi)存,不可以訪問(wèn)其它 線程的工作內(nèi)存。工作內(nèi)存中保存了主內(nèi)存共享變量的副本,線程要操作這些共享變量,只能通過(guò)操作工作內(nèi)存中的副本來(lái)實(shí)現(xiàn),操作完畢之后再同步回到主內(nèi)存當(dāng) 中。如何保證多個(gè)線程操作主內(nèi)存的數(shù)據(jù)完整性是一個(gè)難題,Java內(nèi)存模型也規(guī)定了工作內(nèi)存與主內(nèi)存之間交互的協(xié)議,首先是定義了8種原子操作:
(1) lock:將主內(nèi)存中的變量鎖定,為一個(gè)線程所獨(dú)占
(2) unclock:將lock加的鎖定解除,此時(shí)其它的線程可以有機(jī)會(huì)訪問(wèn)此變量
(3) read:將主內(nèi)存中的變量值讀到工作內(nèi)存當(dāng)中
(4) load:將read讀取的值保存到工作內(nèi)存中的變量副本中。
(5) use:將值傳遞給線程的代碼執(zhí)行引擎
(6) assign:將執(zhí)行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲(chǔ)到主內(nèi)存中。
(8) write:將store存儲(chǔ)的值寫(xiě)入到主內(nèi)存的共享變量當(dāng)中。
我們可以看到,要保證數(shù)據(jù)的同步,lock和unlock定義了一個(gè)線程訪問(wèn)一次共享內(nèi)存的界限,有l(wèi)ock操作也必須有unlock操作,另外一 些操作也必須要成對(duì)出現(xiàn)才可以,像是read和load、store和write需要成對(duì)出現(xiàn),如果單一指令出現(xiàn),那么就會(huì)造成數(shù)據(jù)不一致的問(wèn)題。 Java內(nèi)存模型也針對(duì)這些操作指定了必須滿足的規(guī)則:
(1) read和load、store和write必須要成對(duì)出現(xiàn),不允許單一的操作,否則會(huì)造成從主內(nèi)存讀取的值,工作內(nèi)存不接受或者工作內(nèi)存發(fā)起的寫(xiě)入操作而主內(nèi)存無(wú)法接受的現(xiàn)象。
(2) 在線程中使用了assign操作改變了變量副本,那么就必須把這個(gè)副本通過(guò)store-write同步回主內(nèi)存中。如果線程中沒(méi)有發(fā)生assign操作,那么也不允許使用store-write同步到主內(nèi)存。
(3) 在對(duì)一個(gè)變量實(shí)行use和store操作之前,必須實(shí)行過(guò)load和assign操作。
(4) 變量在同一時(shí)刻只允許一個(gè)線程對(duì)其進(jìn)行l(wèi)ock,有多少次lock操作,就必須有多少次unlock操作。在lock操作之后會(huì)清空此變量在工作內(nèi)存中原 先的副本,需要再次從主內(nèi)存read-load新的值。在執(zhí)行unlock操作前,需要把改變的副本同步回主存。
內(nèi)存可見(jiàn)性
通過(guò)上面Java內(nèi)存模型的概述,我們會(huì)注意到這么一個(gè)問(wèn)題,每個(gè)線程在獲取鎖之后會(huì)在自己的工作內(nèi)存來(lái)操作共享變量,在工作內(nèi)存中的副本回寫(xiě)到主 內(nèi)存,并且其它線程從主內(nèi)存將變量同步回自己的工作內(nèi)存之前,共享變量的改變對(duì)其它線程是不可見(jiàn)的。那么很多時(shí)候我們需要一個(gè)線程對(duì)共享變量的改動(dòng),其它 線程也需要立即得知這個(gè)改動(dòng)該怎么辦呢?比如以下的情景,有一個(gè)全局的狀態(tài)變量open:
boolean open= true;這個(gè)變量用來(lái)描述對(duì)一個(gè)資源的打開(kāi)關(guān)閉狀態(tài),true表示打開(kāi),false表示關(guān)閉,假設(shè)有一個(gè)線程A,在執(zhí)行一些操作后將open修改為false:
//線程A
resource.close(); open = false;線程B隨時(shí)關(guān)注open的狀態(tài),當(dāng)open為true的時(shí)候通過(guò)訪問(wèn)資源來(lái)進(jìn)行一些操作:
//線程B
while(open) { doSomethingWithResource(resource); }當(dāng)A把資源關(guān)閉的時(shí)候,open變量對(duì)線程B不可見(jiàn),如果此時(shí)open變量的改動(dòng)尚未同步到線程B的工作內(nèi)存中,那么線程B就會(huì)用一個(gè)已經(jīng)關(guān)閉了的資源去做一些操作,因此產(chǎn)生錯(cuò)誤。
所以對(duì)于上面的情景,要求一個(gè)線程對(duì)open的改變,其他的線程能夠立即可見(jiàn),Java為此提供了volatile關(guān)鍵字,在聲明open變量的時(shí) 候加入volatile關(guān)鍵字就可以保證open的內(nèi)存可見(jiàn)性,即open的改變對(duì)所有的線程都是立即可見(jiàn)的。volatile保證可見(jiàn)性的原理是在每次 訪問(wèn)變量時(shí)都會(huì)進(jìn)行一次刷新,因此每次訪問(wèn)都是主內(nèi)存中最新的版本。
指令重排序
很多介紹JVM并發(fā)的書(shū)或文章都會(huì)談到JVM為了優(yōu)化性能,采用了指令重排序,但是對(duì)于什么是指令重排序,為什么重排序會(huì)優(yōu)化性能卻很少有提及,其實(shí)道理很簡(jiǎn)單,假設(shè)有這么兩個(gè)共享變量a和b:
PRivate int a; private int b;在線程A中有兩條語(yǔ)句對(duì)這兩個(gè)共享變量進(jìn)行賦值操作:
a = 1; b = 2;假設(shè)當(dāng)線程A對(duì)a進(jìn)行復(fù)制操作的時(shí)候發(fā)現(xiàn)這個(gè)變量在主內(nèi)存已經(jīng)被其它的線程加了訪問(wèn)鎖,那么此時(shí)線程A怎么辦?等待釋放鎖?不,等待太浪費(fèi)時(shí)間了,它會(huì)去嘗試進(jìn)行b的賦值操作,b這時(shí)候沒(méi)被人占用,因此就會(huì)先為b賦值,再去為a賦值,那么執(zhí)行的順序就變成了:
b = 2; a = 1;對(duì)于在同一個(gè)線程內(nèi),這樣的改變是不會(huì)對(duì)邏輯產(chǎn)生影響的,但是在多線程的情況下指令重排序會(huì)帶來(lái)問(wèn)題,看下面這個(gè)情景:
在線程A中:
context = loadContext(); inited = true;在線程B中:
while(!inited ){ sleep } doSomethingwithconfig(context);假設(shè)A中發(fā)生了重排序:
inited = true; context = loadContext();那么B中很可能就會(huì)拿到一個(gè)尚未初始化或尚未初始化完成的context,從而引發(fā)程序錯(cuò)誤。
想到有一條古老的原則很適合用在這個(gè)地方,那就是先要保證程序的正確然后再去優(yōu)化性能。此處由于重排序產(chǎn)生的錯(cuò)誤顯然要比重排序帶來(lái)的性能優(yōu)化要重 要的多。要解決重排序問(wèn)題還是通過(guò)volatile關(guān)鍵字,volatile關(guān)鍵字能確保變量在線程中的操作不會(huì)被重排序而是按照代碼中規(guī)定的順序進(jìn)行訪 問(wèn)。
最后的總結(jié)
這篇文章簡(jiǎn)單的介紹了Java內(nèi)存模型、內(nèi)存可見(jiàn)性和指令重排序。不過(guò)最后看來(lái)其實(shí)主要是在解釋volatile這個(gè)關(guān)鍵字,個(gè)人感覺(jué) volatile關(guān)鍵字是Java當(dāng)中最令人困惑和最難理解的關(guān)鍵字。相對(duì)于synchronized塊的代碼鎖,volatile應(yīng)該是提供了一個(gè)輕量 級(jí)的針對(duì)共享變量的鎖,當(dāng)我們?cè)诙鄠€(gè)線程間使用共享變量進(jìn)行通信的時(shí)候需要考慮將共享變量用volatile來(lái)修飾,對(duì)于需要使用volatile的各種 情景,看到IBM Developer Works上有一篇文章總結(jié)的很不錯(cuò),推薦一下: http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
補(bǔ)充說(shuō)明:64位long和double
在JVM規(guī)范中Java內(nèi)存模型要求lock、unlock、read、load、assign、use、store、write這8個(gè)操作必須是 原子的,但是對(duì)于64位的long和double來(lái)說(shuō),如果沒(méi)有被volatile修飾符修飾,那么可以不是原子的,注意是可以,即虛擬機(jī)在實(shí)現(xiàn)的時(shí)候可 以選擇是否是原子操作。目前幾乎所有的商用虛擬機(jī)都將此實(shí)現(xiàn)為原子操作,因此不必每次用到它們都去加volatile修飾。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注