1. 程式人生 > 程式設計 >「Java併發程式設計實戰」之物件的共享

「Java併發程式設計實戰」之物件的共享

前言

  本系列部落格是對《Java併發程式設計實戰》的一點總結,本篇主要講解以下幾個內容,內容會比較枯燥。可能大家看標題不能能直觀的感受出到底什麼意思,這就是專業術語,哈哈,解釋下,術語(terminology)是在特定學科領域用來表示概念的稱謂的集合,在我國又稱為名詞或科技名詞(不同於語法學中的名詞)。術語是通過語音或文字來表達或限定科學概念的約定性語言符號,是思想和認識交流的工具。我就用白話文來給大家解釋下這些術語。

執行緒安全

  什麼是執行緒安全?這算是老生常談的問題了,相信大家在面試的過程中也遇到過,線上程安全的定義中,最核心的概念就是正確性,如果對執行緒安全性的定義是模糊的,那麼就是缺乏對正確性的清晰定義。正確性的含義是,某個類的行為與其規範完全一致,在良好的規範中通常會定義各種不變性條件來約束物件的狀態,以及定義各種後驗條件來描述物件操作的結果。說白了就是一個類無論是在單執行緒環境還是多執行緒環境中都能正確的執行,那麼這個類就是執行緒安全的。如果線上程交替執行的過程中導致不可預料的結果,那麼就是執行緒不安全的。

可見性

  假如有一個變數,現在對它進行讀寫操作,可見性說的就是當前執行緒對變數的寫操作是否對其它執行緒可見,就是其它執行緒能不能知道你對這個變數做了修改。如果不能保證可見,必須使用同步機制。否則當其他執行緒來讀這個變數的時候,可能會得到一個已經失效的值。這個值就被稱為失效資料。
  在這裡提醒大家,對於非volatile型別的long和double變數JVM允許將64位的讀操作或寫操作分解為兩個32位的操作,當讀一個非volatile型別的long變數時,如果讀寫操作是在不同的執行緒中執行,那麼很可能會讀取到某個值的高32位和另一個值的低32位,所以在多執行緒環境中使用共享可變的long和double等型別的變數時不安全的,除非使用關鍵字volatile來宣告它們,或者用鎖保護起來。

  1. 現在來介紹一下Volatile: Java語言提供了一種稍弱的同步機制,即volatile型別,用來確保將變數的更新操作通知到其他執行緒。使用就是在變數前面加上volatile即可。在 JMM 中,執行緒之間的通訊採用共享記憶體來實現的。volatile 的記憶體語義是:
  • 當寫一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體中的共享變數值,立即重新整理到主記憶體中。
  • 當讀一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體設定為無效,直接從主記憶體中讀取共享變數。
  1. volatile的使用條件:
  • 對變數的寫入操作不依賴變數的當前值,或者你能確保只有單個執行緒更新變數的值。
  • 該變數不會與其他狀態變數一起納入不變性條件中。
  • 在訪問變數時不需要加鎖。

加鎖機制既可以確保可見性又可以確保原子性,而volatile變數只能確保可見性,千萬不要用它來確保原子性操作。

釋出與逸出

釋出一個物件的意思就是使物件能夠在當前作用域之外的程式碼中使用,例如,將一個指向該物件的的引用儲存到其他程式碼可以訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其他類方法中。當某個不應該釋出的物件被髮布時,這種情況被稱為逸出。

執行緒封閉

當訪問共享的可變資料時,通常需要使用同步。一種避免使用同步的方式就是不同享資料,如果僅在單執行緒內訪問資料,就不需要同步,這種技術被稱為執行緒封閉,它是實現執行緒安全性最簡單的方法之一。下面介紹幾種執行緒封閉技術。

  1. Ad-hoc執行緒封閉

    Ad-hoc執行緒封閉是指,維護執行緒封閉性的職責完全有程式實現來承擔。例如可見性修飾符或區域性變數,能將物件封閉到目標執行緒上。事實上對於執行緒封閉物件通常儲存在共有變數中。Ad-hoc執行緒封閉是非常脆弱的,所以程式中儘量少使用它,可以使用以下兩種技術(棧封閉,ThreadLocal)。

  2. 棧封閉

    棧封閉也被成為執行緒內部使用或者執行緒區域性使用,不要與ThredaLocal混淆,比Ad-hoc更易於維護,也更加健壯。在棧封閉中,只能通過區域性變數才能訪問物件。

//虛擬碼
public void test(){
//定義一個變數
Set set ;
// 例項化一個TreeSet物件,並將該物件的一個引用儲存到set中。
set = new TreeSet();
}
複製程式碼

這樣TreeSet物件就被封閉在區域性變數中,因此也被封閉到執行執行緒中,它位於執行執行緒的棧中,其他執行緒無法訪問這個棧。

  1. ThreadLocal

    維持執行緒封閉性的一種更為規範的方法是使用ThreadLocal,這個類能使執行緒中的某個值與儲存值的對像關聯起來,ThreadLocal提供了get與set等訪問介面或方法,這些方法為每個使用該變數的執行緒都存有一份獨立的副本,因此get總是返回由當前執行緒執行set時設定的最新值。ThreadLocal通常用於防止對可變對像的單例項變數或全域性變數進行共享。

   //儲存一個資料庫連線對像
   public static ThreadLocal<Connection> connectionThreadLocal = 
   new ThreadLocal<Connection>(){
       @Override
       protected Connection initialValue() {
           return DriverManager.getConnection(DB_URl);
       }
   };
   //每個執行緒使用時直接get
   public static Connection getConnection(){
       return connectionThreadLocal.get();
   }
複製程式碼

不變性

如果某個對像在被建立之後其狀態就不能被修改,那麼這個物件就是不可變物件,執行緒安全性是是不可變物件的固有屬性之一。當滿足一下條件時,物件才是不可變的:

  • 物件建立後其狀態就不能修改。
  • 物件的所有域都是final型別。
  • 物件是正確建立的(在物件的建立期間,this引用沒有逸出)

Final域: 用於構造不可變物件。final型別的域是不能修改的(但如果final域所引用的物件是可變的,那麼這些引用的物件是可以修改的)。然而在java記憶體模型中,final域還有著特殊的語義。final域能確保初始化過程的安全性,從而可以不受限制的訪問不可變物件,並在共享這些物件時無須同步。

安全釋出

  1. 要安全釋出一個物件,物件的引用以及物件的狀態必須同時對其他執行緒可見,一個正確構造的物件可以通過以下方式來安全的釋出:
  • 在靜態初始化函式中初始化一個物件引用。
  • 將物件的引用儲存到volatile型別的域或者Atomicreferance物件中。
  • 將物件的引用儲存到某個正確構造物件的final型別域中。
  • 將物件的引用儲存到一個由鎖保護的域中。
  1. 在併發程式中使用和共享物件時,可以使用一些實用的策略:
  • 執行緒封閉:執行緒封閉的物件只能由一個執行緒擁有,物件被封閉在該執行緒中,並且只能由這個執行緒修改
  • 只讀共享:在沒有額外同步的情況下,共享的只讀物件可以由多個執行緒併發訪問,但任何執行緒都不能修改它,共享的只讀物件包括不可變物件和事實不可變物件。
  • 執行緒安全共享:執行緒安全的物件在其內部實現同步,因此多執行緒可以通過物件的公有介面來進行訪問而不需要進一步的同步。
  • 保護物件:被保護的物件只能通過持有特定的鎖來訪問,保護物件包括封裝在其他執行緒安全物件中的物件,以及已釋出的並且由某個特定鎖保護的物件。

大家看后辛苦點個贊點個關注哦!檢視個人主頁,有更多的部落格哦。如有錯誤,煩請指正。 有興趣加群一起交流。