1. 程式人生 > >JAVA並發實戰學習筆記——3,4章

JAVA並發實戰學習筆記——3,4章

enc 方法 不變 設定 cti 保護 () col 約束

JAVA並發實戰學習筆記


第三章 對象的共享

  • 失效數據
    • java程序實際運行中會出現①程序執行順序對打亂;②數據對其它線程不可見——兩種情況
    • 上述兩種情況導致在缺乏同步的程序中出現失效數據這一現象,且“失效”這一情況不確定性很大,因為可能出現可能沒出現。
    • JVM中沒有規定對於64位變量如:long, double 的讀寫操作必須是原子的,因此不同步的情況下讀取該類數據可能得到的值無意義(低32位和高32位沒有一起形成完整的數字)
      • 商用的JVM一般會讓64位變量的讀取原子化。
  • 使用volatile修飾變量可以保證變量的可見性,但不會保證互斥性,是比內置鎖更弱的同步機制。
    • 在以下兩種情況下可以使用volatle保證同步。但是過度依賴volatile會使得代碼脆弱、可讀性差。
      • 1.只有一個線程對volatile變量實行寫操作;
      • 2.變量不需要與其它狀態變量共同參與不變約束。
    • 加鎖操作兼保證原子性和可見性

  • 發布(將變量的引用保存到其它代碼可以訪問到的地方)與逸出(不該發布的變量被發布了):

    • 發布的幾種方式:
      • public static A
      • 某個對象A的引用在另一個對象B中,發布B則間接地發布了A
      • 將對象傳遞給外部方法
      • 發布內部類對象A,A中隱含地包含了外部類對象B
  • 逸出

    • 在構造函數中啟動了新線程(該對象的內部線程類),使得this指針在實例沒有建好時便被其它線程共享了。

  • 線程封閉:線程封閉是一類編程的方法,使得各個線程中的對象是不相互共享的,如:JDBC中的Connection對象
    • Ad-hoc線程封閉:盡量少用,十分脆弱
    • 棧封閉:局部變量由於在運行中是處於線程棧的局部變量表和操作數棧中,所以局部變量無論是否是線程安全,只要其引用不發布出去,都是線程安全的。
      • 以能用局部變量就別用全局的變量,全局變量容易引起並發問題。
    • ThreadLocal類線程封閉:可將ThreadLocal t 看成是Map<Thread,T> t,保存了每個線程到T對象的一份副本,t.get()得到的是initialValue()設定的值。
      • 副本保存在線程中,隨著線程的技術被垃圾回收
      • 引入了類之間的耦合性,小心使用
      • ThreadLocal類應用場景示例:移植單線程程序到多線程環境

  • 不變性
    • 滿足下列三個條件的對象滿足“對象不可變
      • 創建好後,不提供更改其狀態的方法
      • 所有的域都是 final型
      • 對象是正確創建的
    • 對象引用不可變”————相比於對象不可變,對象的內容可以改變,但是對象地址無法改變(即用final修飾的變量)
    • 不可變對象為一系列操作提供弱原子性
      • 只要將參數傳入方法,在該方法內就有指向域的引用,其它的線程修改了原對象的域,也不會影響到該方法對原域的訪問。

  • 安全發布————確保可以不受JAVA不可見性的影響,得到發布的最新的對象
    • 不可變對象安全發布的方式
      • final域具有特殊的初始化安全性保證,在初始化的時候即使沒有同步,也可以保證其可見性。
    • 可變對象安全發布的方式
      • 靜態初始化對象(標有static的對象初始化代碼是在類初始化階段執行的,JVM自帶線程安全特性)
      • 將對象的引用保存在volatile類型的域中,或者AtomicReference對象中
      • 對象引用存入final類型域中
      • 將對象引用存入某個由鎖保護的域中
  • 安全訪問
    • 事實不可變對象和不可變對象
      • 任意訪問
    • 可變對象
      • 訪問時需要同步機制,對象需要是線程安全,或者訪問前先獲得某個鎖

第四章 對象的組合


  • 如何建立線程安全的類
    • 1.收集同步需求————候選範圍為對象的域,包括對象中包含的基本類型變量,以及域對象,和域對象內的域
      • 不變性條件
        • 單個變量,即變量的值需要在其合法範圍內
        • 多個變量,即多個變量的值之間需要滿足某些約束
        • 訪問同一個不變性條件中任何一個變量,都需要獲得同一個鎖,以確保對操作不會破壞不變性條件
      • 後驗條件
        • 變量的值轉換需要滿足的約束
      • 不變性條件和後驗條件約束了對象的哪些狀態和狀態轉換是有效的
    • 2.先驗條件————即依賴狀態的操作

  • 實例封閉————使封裝的數據被封閉在另一個對象中,被封閉的對象不超出其作用域????

    • 一般知識

      • 將對實例內封裝的對象的訪問限制在對象的方法上,以確保線程在訪問數據是總能持有正確的鎖.
      • 含有線程不安全的內部對象的線程安全類示例:
        • Collections.synchronized***()方法通過裝飾器模式將容器封裝在一個線程安全的對象中
       
      public class PersonSet{
      
      private final Set myset = new HashSet;
      	public synchronized void addPerson(Person p){
      		myset.add(p);
      	}
      
      	public synchronized boolean containsPerson(Person p){
      		return myset.contains(p);
      	}
      }
      
      • 疑惑之處:
        • 不超出作用域?如何保證方法有合適的返回值呢?覺得很奇怪。
          • 安全發布狀態變量的三個條件
            • 1. 狀態變量是線程安全的
            • 2. 變量不存在不變性約束
            • 3. 在方法中不包含使得該變量進入不合法狀態的操作
        • 而且使用鎖同步,和不使對象溢出有什麽聯系呢?我覺得這就是兩個獨立的東西,放在一起湊成一節,讓人困惑
          • 猜測:要實現非線程封閉的線程安全類,封閉對象是第一步,限制訪問方法並使訪問方法同步是第二步。
    • Java監視器模式(即私有的所對象,而非內置鎖)
      • 好處是不會讓外部的方法得到該對象的鎖
      • 若容器內的對象是非線程安全的,可以每次發布該對象的時候都深度復制

  • 線程安全性的委托
    • 將線程安全性委托給單個/多個(彼此獨立的對象,且所有的方法中都不包含無效狀態轉換操作)線程安全的狀態
    • 若類中包含不符合上一條要求的,包含多個有不
    • 條件約束的狀態,實現線程安全性需要加鎖機制

  • 在現有的線程安全類中添加新功能
    • 在並發中,能用現成的線程安全類就盡量用;若現成的類滿足不了需求,則可能選擇添加新功能
    • 四種方法:
      • 改源碼 ————不現實
      • 擴展基礎類 ———— 在自己的子類新添加的方法中使用內置鎖——從這裏來看,子類和父類中使用的內置鎖應該是同一個,都是真對實例而言的。
        • 缺點:破壞了類的封裝性
      • 客戶端加鎖機制 ———— 了解基礎類對象使用的是什麽鎖,在客戶端代碼中使用相同的鎖
        • 缺點:破壞了同步策略的封裝性
      • 組合模式 ————使用裝飾器模式將基礎類如:list等封裝在內部,將list的方法包裝一層,使用裝飾器類的內置鎖。
        • 優點
          • 不會破壞封裝性
          • 健壯性更強
          • 即使容器類不是線程安全的,也可以借此實現線程安全
        • 缺點
          • 多加一層鎖,效率下降

  • 文檔!!!文檔!!!————要想使開發和用戶使用遵從本類的安全機制,必須有文檔記錄
    • 設計文檔 & 用戶文檔
    • Java文檔中應該註明該類使用的同步機制,應該包含下面一些內容
      • 是否是線程安全的?
      • 客戶回調需不需要加鎖,可以加那些鎖?
      • 哪些鎖保護了哪些狀態?(設計文檔,可以用java註釋便於後續開發)

JAVA並發實戰學習筆記——3,4章