天天看點

java并發程式設計讀書筆記(1)-- 對象的共享

RIM(Remote Method Invocation):遠端方法調用

Race Condition:競态條件

Servlet要滿足多個線程的調用,必須是線程安全的

遠端對象,即通過遠端方法調用将對象放入位元組流中傳給其他jvm的對象,要特别注意對象中的共享狀态

Shared:共享的

Mutable:可變的

當設計線程安全的類時,良好的面向對象技術、不可修改性,以及明晰的不變性規範都能起到一定的幫助作用;

無狀态對象是線程安全的:沒有任何域也不包含任何對其他類中域的引用(比如StatelessFactory implements Servlet),多個線程通路并沒有共享狀态,不會影響其正确性。

最常見的一個靜态類型是:先檢查後執行(Check-Then-Act)操作,即通過一個可能失效的觀測結果來決定下一步的動作。

要保持狀态的一緻性,就需要自單個原子操作中更新所有相關的狀态變量

内置鎖(Intrinsic Lock),監視鎖(Monitor Lock):每個對象都可以當做。

重入:當某個線程請求一個由其他線程持有的鎖時會被阻塞,但請求他自己持有的鎖就會成功。内置鎖是可重入鎖。

子類繼承了父類,重寫了父類的synchronized方法,通路子類的這個方法時要先獲得父類的鎖,然後擷取自身的鎖。如果在這個方法裡又調用了父類的這個方法(super.xx),可以繼續說明,擷取的父類鎖可以重入。

對于每個包含多個變量的不變條件,其中涉及的多有變量都需要由同一個鎖來保護

java記憶體模型要求變量的讀取操作和寫入操作都必須是原子操作,但對于非volatile類型的64位數值變量(double,long),jvm允許将64位的讀操作或寫操作分解為兩個32位的操作。當讀取一個非volatile類型的long變量時,如果對該變量的讀操作和寫操作在不同的線程中執行,那麼很可能會讀取到某個值的高32位和另一個值的低32位。是以,即使不考慮失效資料問題,在多線程程式中使用共享且可變的long和double等類型的變量也是不安全的,除非使用volatile來聲明他們或者用鎖保護起來。

加鎖的含義不僅僅局限于互斥行為,還包括記憶體可見性。為了確定所有的線程都能看到共享變量的最新值,所有的執行讀操作或寫操作的線程都必須在同一個鎖上同步。

把變量聲明為volatile類型後,編譯與運作時都會注意到這個變量是共享的,是以不會講該變量上的操作與其他記憶體操作一起重排序。volatile變量不會被緩存到寄存器或者對其他處理器不可見的地方,是以在讀取volatile變量時總會傳回最新寫入的值。

volatile的一個用法:while的條件變量,為保證可見性。

加鎖機制既可以確定可見性又可以確定原子性,而volatile變量隻能確定可見性。

釋出(Publish)一個對象:是對象能夠在目前作用域之外的代碼中使用。

逸出(Escape):當某個不應釋出的對象被釋出時。

不要再構造過程中使this逸出。

對變量的寫入操作不依賴變量的目前值,或者你能確定隻有單個線程更新變量的值。

該變量不會與其他狀态變量一起納入不變性條件中

在通路變量時不需要加鎖

比如,while循環的條件變量,如果監視是否改變,需要設定為volatile,否則通路while的線程會将asleep放到工作區,一直循環直到某刻去記憶體讀取。

釋出(Publish)一個對象是指:使對象能夠在目前作用域之外的代碼中使用。 在許多情況,我們要確定對象及其内部狀态不被釋出。而在某些情況,我又需要釋出該對象,但如果在釋出時要確定線程安全性,則可能需要同步。釋出内部狀态可能會破壞封裝性,使線程難以維持不變的狀态。例如,如果在對象構造完成之前就釋出該對象,就會破壞線程安全性。 釋出對象的最簡單的方法是将對象的引用儲存到一個共有的靜态變量中。 逸出(Escape):當某個不應該釋出的對象呗釋出時。

  如果按上述的方式釋出states,則任何調用者都可以修改數組,這是不安全的。

java并發程式設計讀書筆記(1)-- 對象的共享

 上述構造函數好像沒啥問題,至少我看本書之前看不出。構造函數中的對象是this,其他譬如引用類屬性的擁有者為this,如果将這哥匿名類傳遞給source,source如果對其進行了引用,而這時候構造函數還沒結束即沒有建立ThisEscape的對象,這個匿名類也還沒構造,即空。當且僅當對象的構造函數傳回時,對象才處于一個可預測和一緻的狀态。

參考http://bruce008.iteye.com/blog/1461345

文章指出,匿名内部類編譯的結果表明ThisEscape這個類會作為一個final成員變量放到匿名内部類中,即這個ThisEscape被逃逸出去了。是以不要再構造函數中釋出匿名類和起線程。

 當某個對象封閉在一個線程中時,這種方法将自動實作線程安全性,即使被封閉的對象本身不是線程安全的。

  如果線上程内部(Within-Thread)上下文中使用非線程安全的對象,那麼該對象仍然是線程安全的。但要注意後期維護的時候對象逸出。

維持線程封閉性的一種更規範方法是使用ThreadLocal。ThreadLocal對象通常用于防止對可變的單執行個體變量(Singleton)或全局變量進行共享。比如,在單線程應用中可能會維持一個全局的資料庫連接配接,并在程式啟動時初始化這個連接配接對象,進而避免在調用每個方法時都要傳遞一個connection對象。由于jdbc的連接配接對象不一定是線程安全的,是以當多線程應用程式在沒有協同的情況下使用全局變量時,就不是線程安全的。通過将jdbc的連接配接儲存到ThreadLocal對象中,每個線程都會擁有屬于自己的連接配接:

  當某個頻繁執行的操作需要一個臨時對象,例如一個緩沖區,而同時又希望避免在每次執行時都重新配置設定該臨時對象,就可以使用這項技術。

當某個線程初次調用ThreadLocal.get方法時,就會調用initialValue來擷取初始值。

ThreadLocal變量類似于全局比阿娘,它能降低代碼的可重用性,并在類之間引入隐含的耦合性,是以在使用時要格外小心。

滿足同步需求的另一種方法是使用不可變對象(Immutable Object)。如果某個對象在被建立後其狀态就不能修改,那麼這個對象就成為不可變對象。

對象建立後其狀态就不能修改。final

對象的所有域都是final

對象是正确建立的(在建立對象期間,this沒有逸出)

 正如除非需要更高的可見性,否則應将所有的域都聲明為私有域是一個良好的習慣,除非需要某個域是可變的,否則應将其聲明為final域也是一個良好的程式設計習慣。

在某些情況下,我們希望在多個線程間共享對象,此時必須確定安全地進行共享。以下是錯誤的:

  看起來應該沒什麼問題才對,實際上多線程通路的時候可能得到一個尚未建立的對象。

再詳細些:

  除了建立hoder的線程,其他線程擷取holder的狀态是不一定的。也許是建立前獲得為null,也許是建立了還沒指派,等等。這是不安全不正确的釋出。

 要安全的釋出一個對象,對象的引用以及對象的狀态必須同時對其他線程可見。一個正确構造的對象可以通過以下方式來安全地釋出: 在今天初始化函數中初始化一個對象引用。 将對象的引用儲存到volatile類型的域或者atomicReferance對象中 将對象的引用儲存到某個正确構造對象的final類型域中 将對象的引用儲存到一個由鎖保護的域中 對象的釋出需求取決于它的可變性: 不可變對象可以通過任意機制來釋出。 事實不可變對象(對象從技術上來看是可變的,但其狀态在釋出後不會再改變)必須通過安全方式來釋出 可變對象必須通過安全方式來釋出,并且必須是線程安全的或者由某個鎖保護起來。

當獲得對象的一個引用時,你需要知道在這個引用閃個可以執行哪些操作。在使用它之前手否需要獲得一個鎖?是否可以修改它的狀态,或者隻能讀取它?許多并發錯誤都是由于沒有了解共享對象的這些“既定規則”而導緻的。當釋出一個對象時,必須明确地說明對象的通路方式。

在并發程式中使用和共享對象時,可以使用一些使用的政策,包括: 線程封閉:線程封閉的對象隻能由一個線程擁有,對象被封閉在該線程中,并且隻能由這個線程修改。 隻讀共享:在沒有額外同步的情況下,共享的隻讀對象可以由多個線程并發通路,但很任何線程都不能修改它。共享的隻讀對象包括不可變對象和事實不可變對象。 線程安全共享:線程安全的對象在其内部實作同步,是以多個線程可以通過對象的公有接口來進行通路而不需要進一步的同步歐。 保護對象:被保護的對象隻能通過持有特定的鎖來通路。保護對象包括封裝在其他線程安全對象中的對象,以及已釋出的并且由某個特定鎖保護的對象。

唯有不斷學習方能改變!

-- <b>Ryan Miao</b>