Java并發(fā)編程實(shí)戰(zhàn) Chapt3 對象的共享

同步:

  • 原子性
  • 內(nèi)存可見性

3.1 可見性

重排序(Reordering):在沒有同步的情況下,編譯器、處理器以及運(yùn)行時(shí)等都可能對操作的執(zhí)行順序進(jìn)行一些意想不到的調(diào)整。
只要有數(shù)據(jù)在多個(gè)線程之間共享,就使用正確的同步。

3.1.1 失效數(shù)據(jù)

3.1.2 非原子的64位操作

最低安全性:當(dāng)線程在沒有同步的情況下讀取變量時(shí),可能會(huì)得到一個(gè)失效值,但至少這個(gè)值是由之前某個(gè)線程設(shè)置的值,而不是一個(gè)隨機(jī)值。
最低安全性不適用于非volatile類型的64位數(shù)值變量(double和long)。變量的讀取和寫入操作都必須是原子操作,但對于非volatile類型的long和double變量,JVM允許將64位的讀操作或?qū)懖僮鞣纸鉃閮蓚€(gè)32位的操作。當(dāng)讀取一個(gè)非volatile類型的long變量時(shí),如果對該變量的讀操作和寫操作在不同的線程中執(zhí)行,那么很可能會(huì)讀取到某個(gè)值的高32位和另一個(gè)值的低32位。

3.1.3 加鎖和可見性

加鎖的含義不僅僅局限于互斥行為,還包括內(nèi)存可見性。為了確保所有線程都能看到共享變量的最新值,所有執(zhí)行讀操作或者寫操作的線程都必須在同一個(gè)鎖上同步。

3.1.4 volatile變量

當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時(shí)(虛擬機(jī))都會(huì)注意到這個(gè)變量是共享的,因此不會(huì)將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會(huì)被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時(shí)總會(huì)返回最新的寫入值。
在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也就不會(huì)使執(zhí)行線程阻塞,因此volatile變量是一種比synchronized關(guān)鍵字更輕量級的同步機(jī)制。
寫入volatile變量相當(dāng)于退出同步塊,而讀取volatile變量相當(dāng)于進(jìn)入同步塊。
不建議過度依賴volatile變量提供的可見性,如果在代碼中依賴volatile變量來控制狀態(tài)的可見性,通常比使用鎖的代碼更脆弱,也更難以理解。
僅當(dāng)volatile變量能簡化代碼的實(shí)現(xiàn)以及對同步策略的驗(yàn)證時(shí),才應(yīng)該使用它們。如果在驗(yàn)證正確性時(shí)需要對可見性進(jìn)行復(fù)雜的判斷,那么就不要使用volatile變量。volatile變量的正確使用方式包括:確保它們自身狀態(tài)的可見性,確保它們所引用對象的狀態(tài)的可見性,以及標(biāo)識一些重要的程序生命周期事件的發(fā)生。
典型用法:


加鎖機(jī)制既可以確保可見性又可以確保原子性,而volatile變量只能確保可見性。
當(dāng)且僅當(dāng)滿足以下所有條件時(shí),才應(yīng)該使用volatile變量:**

  • 對變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個(gè)線程更新變量的值。(非競爭條件)
  • 該變量不會(huì)與其他狀態(tài)變量一起納入不變性條件中。(非復(fù)合操作)
  • 在訪問變量時(shí)不需要加鎖。

3.2 發(fā)布與逸出

發(fā)布:使對象能夠在當(dāng)前作用域之外的代碼中使用
逸出:某個(gè)不應(yīng)該發(fā)布的對象被發(fā)布
發(fā)布對象:

  • 將對象的引用保存到一個(gè)公有的靜態(tài)變量中
  • 發(fā)布一個(gè)對象時(shí),在該對象的非私有域中引用的所有對象同樣會(huì)被發(fā)布
  • 發(fā)布一個(gè)內(nèi)部類的實(shí)例(內(nèi)部類持有外部類的引用)

安全的對象構(gòu)造過程

當(dāng)從對象的構(gòu)造函數(shù)中發(fā)布對象時(shí),只是發(fā)布了一個(gè)尚未構(gòu)造完成的對象。即使發(fā)布對象的語句位于構(gòu)造函數(shù)的最后一行也是如此。如果this引用在構(gòu)造過程中逸出,那么這種對象就被認(rèn)為是不正確構(gòu)造。
不要在構(gòu)造過程中使this引用逸出。
在構(gòu)造函數(shù)中調(diào)用一個(gè)可改寫的實(shí)例方法時(shí)(非private,非final),同樣會(huì)導(dǎo)致this引用在構(gòu)造過程中逸出。
如果想在構(gòu)造函數(shù)中注冊一個(gè)事件監(jiān)聽器或啟動(dòng)線程,那么可以使用一個(gè)私有的構(gòu)造函數(shù)和一個(gè)公共的工廠方法,從而避免不正確的構(gòu)造過程。

3.3 線程封閉

線程封閉:僅在單線程內(nèi)訪問數(shù)據(jù)
Java語言及其核心庫提供了一些機(jī)制來幫助維持線程封閉性,例如局部變量和ThreadLocal類,但即便如此,程序員仍然需要負(fù)責(zé)確保封閉在線程中的對象不會(huì)從線程中逸出。**

3.3.1 Ad-hoc線程封閉

Ad-hoc線程封閉:維護(hù)線程封閉性的職責(zé)完全由程序?qū)崿F(xiàn)來承擔(dān)
當(dāng)決定使用線程封閉技術(shù)時(shí),通常是因?yàn)橐獙⒛硞€(gè)特定的子系統(tǒng)實(shí)現(xiàn)為一個(gè)單線程子系統(tǒng)。在某些情況下,單線程子系統(tǒng)提供的簡便性要?jiǎng)龠^Ad-hoc線程封閉性技術(shù)的脆弱性。
只要確保只有單個(gè)線程對共享的volatile變量執(zhí)行寫入操作,那么就可以安全地在這些共享的volatile變量上執(zhí)行“讀取-修改-寫入”的操作。這相當(dāng)于將修改操作封閉在單個(gè)線程中以防止發(fā)生競爭條件,并且volatile變量的可見性保證還確保了其他線程能看到最新的值。**
由于Ad-hoc線程封閉技術(shù)的脆弱性,因此在程序中盡量少用它,在可能的情況下,應(yīng)該使用更強(qiáng)的線程封閉技術(shù)(棧封閉或ThreadLocal類)。**

3.3.2 棧封閉

在棧封閉中,只能通過局部變量才能訪問對象。
局部變量的固有屬性之一就是封閉在執(zhí)行線程中。它們位于執(zhí)行線程的棧中,其他線程無法訪問這個(gè)棧。
棧封閉比Ad-hoc線程封閉更易于維護(hù),也更加健壯。
基本類型的局部變量始終封閉在線程內(nèi)。
如果線程內(nèi)部上下文中使用非線程安全的對象,那么該對象仍然是線程安全的。**

3.3.3 ThreadLocal類

ThreadLocal對象通常用于防止對可變的單實(shí)例變量或全局變量進(jìn)行共享。
當(dāng)某個(gè)頻繁執(zhí)行的操作需要一個(gè)臨時(shí)對象,例如一個(gè)緩沖區(qū),而同時(shí)又希望避免在每次執(zhí)行時(shí)都重新分配該臨時(shí)對象,就可以使用這項(xiàng)技術(shù)。
ThreadLocal變量類似于全局變量,它能降低代碼的可重用性,并在類之間引入隱含的耦合性,因此在使用時(shí)要格外小心。

3.4 不變性

不可變對象一定是線程安全的。
當(dāng)滿足以下條件時(shí),對象才是不可變的:

  • 對象創(chuàng)建以后其狀態(tài)就不能修改
  • 對象的所有域都是final類型(從技術(shù)上看,不可變對象并不需要將其所有的域都聲明為final類型)
  • 對象是正確創(chuàng)建的(在對象創(chuàng)建期間,this引用沒有逸出)

在不可變對象的內(nèi)部仍可以使用可變對象來管理它們的狀態(tài)。

3.4.1 final域

在java內(nèi)存模型中,final域能確保初始化過程的安全性,從而可以不受限制地訪問不可變對象,并在共享這些對象時(shí)無須同步。
即使對象是可變地,通過將對象的某些域聲明為final類型,仍然可以簡化對狀態(tài)的判斷。
除非某個(gè)域是可變的,否則將其聲明為final域。

3.4.2 示例:使用volatile類型來發(fā)布不可變對象

每當(dāng)需要對一組相關(guān)數(shù)據(jù)以原子方式執(zhí)行某個(gè)操作時(shí),就可以考慮創(chuàng)建一個(gè)不可變的類來包含這些數(shù)據(jù)。**
對于在訪問和更新多個(gè)相關(guān)變量時(shí)出現(xiàn)的競爭條件問題,可以通過將這些變量全部保存在一個(gè)不可變對象中來消除。
如果是一個(gè)可變的對象,那么就必須使用鎖來確保原子性。如果是一個(gè)不可變對象,那么當(dāng)線程獲得了對該對象的引用后,就不必?fù)?dān)心另一個(gè)線程會(huì)修改對象的狀態(tài)。



如果要更新這些變量,那么可以創(chuàng)建一個(gè)新的容器對象,但其他使用原有對象的線程仍然會(huì)看到對象處于一致的狀態(tài)。**
通過使用包含多個(gè)狀態(tài)變量的容器對象(不可變對象)來維持不變性條件,并使用一個(gè)volatile類型的引用來確保可見性,使得在沒有顯式地使用鎖的情況下仍然是線程安全的。**

3.5 安全發(fā)布

3.5.1 不正確地發(fā)布:正確的對象被破壞



沒有使用同步確保可見性

3.5.2 不可變對象與初始化安全性

由于不可變對象是一種非常重要的對象,因此Java內(nèi)存模型為不可變對象的共享提供了一種特殊的初始化安全性保證。
任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發(fā)布這些對象時(shí)沒有使用同步。
這種保證還將延伸到被正確創(chuàng)建對象中所有final類型的域。在沒有額外同步的情況下,也可以安全地訪問final類型的域。然而,如果final類型的域所指向的是可變對象,那么在訪問這些域所指向的對象的狀態(tài)時(shí)仍然需要同步。

3.5.3 安全發(fā)布的常用模式

可變對象在發(fā)布和使用的線程都必須使用同步。
要安全地發(fā)布一個(gè)對象,對象的引用以及對象的狀態(tài)必須同時(shí)對其他線程可見。一個(gè)正確構(gòu)造的對象可以通過以下方式來安全地發(fā)布:

  • 在靜態(tài)初始化函數(shù)中初始化一個(gè)對象引用。
  • 將對象的引用保存到volatile類型的域或者AtomicReference對象中。
  • 將對象的引用保存到某個(gè)正確構(gòu)造對象的final類型域中。
  • 將對象的引用保存到一個(gè)由鎖保護(hù)的域中(在線程安全容器內(nèi)部的同步)。

要發(fā)送一個(gè)靜態(tài)構(gòu)造的對象,最簡單和最安全的方式是使用靜態(tài)的初始化器。


靜態(tài)初始化器由JVM在類的初始化階段執(zhí)行。由于在JVM內(nèi)部存在著同步機(jī)制,因此通過這種方式初始化的任何對象都可以被安全地發(fā)布。

3.5.4 事實(shí)不可變對象

事實(shí)不可變對象(Effectively Immutable Object):對象從技術(shù)上來看是可變地,但其狀態(tài)在發(fā)布后不會(huì)再改變。
程序只需將之視為不可變對象即可。
在沒有額外同步地情況下,任何線程都可以安全地使用被安全發(fā)布的事實(shí)不可變對象。

3.5.5 可變對象

對象的發(fā)布需求取決于它的可變性:

  • 不可變對象可以通過任意機(jī)制來發(fā)布。
  • 事實(shí)不可變對象必須通過安全方式來發(fā)布。
  • 可變對象必須通過安全方式來發(fā)布,并且必須是線程安全的或者由某個(gè)鎖來保護(hù)起來。

3.5.6 安全地共享對象

當(dāng)發(fā)布一個(gè)對象時(shí),必須明確地說明對象的訪問方式。

并發(fā)中使用和共享對象的實(shí)用策略:

  • 線程封閉。
  • 只讀共享。共享的只讀對象包括不可變對象和事實(shí)不可變對象。
  • 線程安全共享。線程安全的對象在其內(nèi)部實(shí)現(xiàn)同步,因此多個(gè)線程可以通過對象的公有接口來進(jìn)行訪問而不需要進(jìn)一步的同步。
  • 保護(hù)對象。被保護(hù)的對象只能通過持有特定的鎖來訪問。保護(hù)對象包括封裝在其他線程安全對象中的對象,以及已發(fā)布的并且由某個(gè)特定鎖保護(hù)的對象。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,204評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內(nèi)容