本文是由JR主持寫作的《J2SE進(jìn)階》一書的部分章節(jié)整理而成,《J2SE進(jìn)階》正在寫作、完善階段。您閱讀后,有任何建議、批評,請 和我聯(lián)系 ,或在 這兒留言 。《J2SE進(jìn)階》寫作項(xiàng)目組感謝您閱讀本文。
java在語言層次上實(shí)現(xiàn)了對線程的支持。它提供了Thread/Runnable/ThreadGroup等一系列封裝的類和接口,讓程序員可以高效的開發(fā)Java多線程應(yīng)用。為了實(shí)現(xiàn)同步,Java提供了synchronize要害字以及object的wait()/notify()機(jī)制,可是在簡單易用的背后,應(yīng)藏著更為復(fù)雜的玄機(jī),很多問題就是由此而起。
在了解Java的同步秘密之前,先來看看JMM(Java Memory Model)。
Java被設(shè)計(jì)為跨平臺的語言,在內(nèi)存治理上,顯然也要有一個(gè)統(tǒng)一的模型。而且Java語言最大的特點(diǎn)就是廢除了指針,把程序員從痛苦中解脫出來,不用再考慮內(nèi)存使用和治理方面的問題。
可惜世事總不盡如人意,雖然JMM設(shè)計(jì)上方便了程序員,但是它增加了虛擬機(jī)的復(fù)雜程度,而且還導(dǎo)致某些編程技巧在Java語言中失效。
JMM主要是為了規(guī)定了線程和內(nèi)存之間的一些關(guān)系。對Java程序員來說只需負(fù)責(zé)用synchronized同步要害字,其它諸如與線程/內(nèi)存之間進(jìn)行數(shù)據(jù)交換/同步等繁瑣工作均由虛擬機(jī)負(fù)責(zé)完成。如圖1所示:根據(jù)JMM的設(shè)計(jì),系統(tǒng)存在一個(gè)主內(nèi)存(Main Memory),Java中所有變量都儲存在主存中,對于所有線程都是共享的。每條線程都有自己的工作內(nèi)存(Working Memory),工作內(nèi)存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內(nèi)存中進(jìn)行,線程之間無法相互直接訪問,變量傳遞均需要通過主存完成。
圖1 Java內(nèi)存模型示例圖
線程若要對某變量進(jìn)行操作,必須經(jīng)過一系列步驟:首先從主存復(fù)制/刷新數(shù)據(jù)到工作內(nèi)存,然后執(zhí)行代碼,進(jìn)行引用/賦值操作,最后把變量內(nèi)容寫回Main Memory。Java語言規(guī)范(JLS)中對線程和主存互操作定義了6個(gè)行為,分別為load,save,read,write,assign和use,這些操作行為具有原子性,且相互依靠,有明確的調(diào)用先后順序。具體的描述請參見JLS第17章。
我們在前面的章節(jié)介紹了synchronized的作用,現(xiàn)在,從JMM的角度來重新審閱synchronized要害字。
假設(shè)某條線程執(zhí)行一個(gè)synchronized代碼段,其間對某變量進(jìn)行操作,JVM會依次執(zhí)行如下動(dòng)作:
(1) 獲取同步對象monitor (lock)
(2) 從主存復(fù)制變量到當(dāng)前工作內(nèi)存 (read and load)
(3) 執(zhí)行代碼,改變共享變量值 (use and assign)
(4) 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容 (store and write)
(5) 釋放同步對象鎖 (unlock)
可見,synchronized的另外一個(gè)作用是保證主存內(nèi)容和線程的工作內(nèi)存中的數(shù)據(jù)的一致性。假如沒有使用synchronized要害字,JVM不保證第2步和第4步會嚴(yán)格按照上述次序立即執(zhí)行。因?yàn)楦鶕?jù)JLS中的規(guī)定,線程的工作內(nèi)存和主存之間的數(shù)據(jù)交換是松耦合的,什么時(shí)候需要刷新工作內(nèi)存或者更新主內(nèi)存內(nèi)容,可以由具體的虛擬機(jī)實(shí)現(xiàn)自行決定。假如多個(gè)線程同時(shí)執(zhí)行一段未經(jīng)synchronized保護(hù)的代碼段,很有可能某條線程已經(jīng)改動(dòng)了變量的值,但是其他線程卻無法看到這個(gè)改動(dòng),依然在舊的變量值上進(jìn)行運(yùn)算,最終導(dǎo)致不可預(yù)料的運(yùn)算結(jié)果。
這一節(jié)我們要討論的是一個(gè)讓Java丟臉的話題:DCL失效。在開始討論之前,先介紹一下LazyLoad,這種技巧很常用,就是指一個(gè)類包含某個(gè)成員變量,在類初始化的時(shí)候并不立即為該變量初始化一個(gè)實(shí)例,而是等到真正要使用到該變量的時(shí)候才初始化之。
例如下面的代碼:
代碼1
class Foo {
PRivate Resource res = null ;
public Resource getResource() {
if (res == null )
res = new Resource();
return res;
}
}
由于LazyLoad可以有效的減少系統(tǒng)資源消耗,提高程序整體的性能,所以被廣泛的使用,連Java的缺省類加載器也采用這種方法來加載Java類。
在單線程環(huán)境下,一切都相安無事,但假如把上面的代碼放到多線程環(huán)境下運(yùn)行,那么就可能會出現(xiàn)問題。假設(shè)有2條線程,同時(shí)執(zhí)行到了if(res == null),那么很有可能res被初始化2次,為了避免這樣的Race Condition,得用synchronized要害字把上面的方法同步起來。代碼如下:
代碼2
Class Foo {
Private Resource res = null ;
Public synchronized Resource getResource() {
If (res == null )
res = new Resource();
return res;
}
}
新聞熱點(diǎn)
疑難解答
圖片精選