程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 並發編程初探-對象的共享,並發編程初探對象

並發編程初探-對象的共享,並發編程初探對象

編輯:JAVA綜合教程

並發編程初探-對象的共享,並發編程初探對象


對象的共享

   一、可見性

    在沒有同步的情況下,編譯器以及運行時等收可能對操作的執行順序進行一些意想不到的調整,在缺乏足夠同步的多線程程序中,要想對內存操作的執行順序進行判斷,幾乎無法得到正確的結論。

    1.缺乏同步的程序中可能出產生錯誤結果的一種情況:失效數據

    2.非原子操作

      當線程在沒有同的情況下讀取變量時,可能會得到一個失效值,但至少這個值是由某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱為最低安全性。

      最低安全性適用於絕大多數變量,但是存在一個例外:非volatile類中的64位數值變量。

      Java內存模型要求,變量的讀取操作和寫入操作都必須是原子操作,但對非volatile類型的long和double變量,JVM允許將64位的讀操作或寫操作分解為兩個32位操作。

      當讀取一個非volatile類型的long變量時,如果對該變量的讀操作和寫操作在不同的線程中執行,俺麼很可能會讀到某個值得高32位和另一個值得低32位。因此,即使不考慮失效數據問題,在多線程程序中使用共享可變的long和double等類型的變量也是不安全的。除非用關鍵字volatile來聲明他們,或者用鎖來保護起來。

    3.加鎖與可見性

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

    4.volatile變量

      volatile變量,用來確保將變量的更新操作通知到其他線程。當變量聲明為volatile類型後,編譯器與運行時都會注意到這個變量是共享德的,因此不會將該變量上的操作與其他內存操作儀器重排序。

      volatile變量不會被緩存在寄存器或者其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

      在訪問volatile變量時會執行加鎖操作,因此,也就不會執行線程阻塞,因此volatile變量時一種比synchronized關鍵字更輕量級的同步機制。

      僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。如果在驗證正確性時需要對可見性進行復雜的判斷,那麼久不要使用volatile變量。volatile變量的正確性使用方式包括:確保它們自身狀態的可見性,確保它們所引用對象的狀態的可見性,以及標識一些更重要的程序生命周期事件的發生(例如,初始化或關閉)。

      volatile變量的一種典型用法:檢查某個標記狀態以判斷是否退出循環。

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

      當且僅當滿足一下所有條件時,才因該使用volatile變量:

        a)對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。

        b)該變量不會與其他裝調變量一起納入不變性條件中

        c)在訪問變量時不需要加鎖

   二、發布與逸出

    發布(Publish)一個對象的意思是指,是對象能夠在當前作用域之外的代碼中使用。

    當某個不應該發布的對象被發布時,這種情況就被成為逸出(Escape)。 

    發布對象的最簡單的方法是將對象的引用保存到一個公有的靜態變量中,以便任何類和線程都能看見該對象。

    1.安全對象的構造過程

     不要在構造過程中使this引用逸出。

     當對象在其構造函數中創建一個線程時,無論是顯式創建(通過將它傳給構造函數)還是隱式創建(由Thread或Runnable是該對象的一個內部類),this引用都會被新創建的線程共享。在對象尚未完全構建之前,先的線程就可以看見它。在構造函數中創建線程並沒有錯,但是最好不要立即啟動它,而是通過一個start或initialize方法來啟動。在構造函數中調用一個不可改寫的實例方法時,同樣會導致this引用在構造過程中逸出。

 

  三、線程封閉

    當訪問共享的可變數據時,通常需要使用同步。一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不需要同步,這種技術稱為線程封閉(Thread Confinement) ,它是實現線程安全性的最簡單方式之一。當某一個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即使被封閉的對象本身不是線程安全的。

    在Swing中大量使用了線程封閉技術。

    線程封閉技術另一種常見應用使JDBC的connection對象,JDBC規范並要求connection對象必須是線程安全的。在典型的服務器應用程序中,線程從連接池中獲取一個connection對象,並且用該對象來處理請求,使用完成後再將對象返回給連接池。由於大多數請求都是由的單個線程采用同步的方式來處理,並且在connection對象返回之前,連接池不會再將它分配給其他線程,因此,這種連接管理模式在處理請求時隱含地將connection對象封閉在線程中。

    在Java語言中並沒有強制規定某個變量必須鎖來保護,同樣在Java語言中也沒有強制將一個對象封閉在 某個線程中。線程封閉式在程序設計中的一個考慮因素,必須在程序中實現,Java語言及核心庫提供了一些機制來幫助維持線程封閉性,例如局部變量和ThreadLocal類,但即便如此,程序員仍然需要負責確保在線程中的對象不會從線程中逸出。

    1.Ad_hoc線程封閉

    Ad_hoc線程封閉是指,維護線程封閉性的職責完全由程序實現類承擔,Ad_hoc線程封閉式非常脆弱的,因為沒有任何一種語言特性,例如可見性修飾符或局部變量,能將對象封閉到目標線程上。

    當決定使用線程封閉技術時,通常是因為要將某個特定的子系統實現為一個單線程子系統。在某種情況下,單線程子系統提供的簡便性要勝過Ad_hoc線程封閉技術的脆弱性。使用單線程子系統的另一個原因是為了避免死鎖。

    2.棧封閉

    棧封閉是線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象局部變量的固有屬性就是封閉在程序的執行線程之中。

    如果在線程內部上下文中使用非線程安全的對象,那麼該對象仍然是線程安全的。

    

    3.ThreadLocal類

    維持線程封閉的一種更規范的方法是ThrashLocal,這個類能使線程中的某個值域保存值得對象關聯起來。ThreadLocal提供了get和set等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。

    ThreadLocal對象通常用於防止對可能的單實例變量或全局變量進行共享。

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

    ThreadLocal變量類似於全局變量,它能降低代碼可重用性,並在類之間引入隱含的耦合性,因此在使用時要格外小心。

 

    4.不變性

     滿足同步的另一種方法是使用不可變對象(Immutable Object)。

    如果某個對象被創建後其狀態不能被修改,那麼這個對象就被稱為不可變對象。線程安全性是不可變對象的固有屬性之一,它們的不變性條件是由構造函數創建的,只要它們不改變,那麼這些不變性條件就能得以維持。

    當滿足以下條件時,對象才是不可變的:

    1)對象創建以後其狀態就不能修改

    2)對象的所有域都是final類型

    3)對象是正確創建的(在對象的創建期間,this引用沒有逸出)

    final域:關鍵字final可以視為C++中const機制的一種受限版本,用於構造不可變對象。final類型的域是不能修改的(但是如果final域引用的對象是可變的,那麼這些被引用的對象是可以修改的)。final域能確保初始化過程的安全性,從而可以不受限制地訪問不可變對象,並在共享這些對象時無需同步。除非需要更高的可見性,否則應將所有的域都聲明為私有域。除非需要某個域是可變的,否則應將其聲明為final域。

    對於在訪問和更新多個相關變量時出現的競爭問題,可以通過將這些變量全部保存在一個不可變對象中來消除,如果是一個可變的對象,那麼當線程獲得了該對象的引用後,就不必擔心另一個線程會修改對象的狀態。如果要更新這些變量,那麼可以創建一個新的容器,但其他使用原有對象的線程,仍然會看到對象處於一致的狀態。

    

    5.安全發布

    不正確的發布:正確的對象被破壞。不能指望一個尚未被完全創建的對象擁有完整性。

    不可變對象與初始化安全性:任何線程都可以在需要額外的同步的情況下安全地訪問不可變對象,即使在發布這些對象是沒有使用任何同步。

    1.安全發布的常用模式:

    1)要安全發布一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確的構造的對象可以通過一下的方式來安全地發布:

      a)在靜態初始化函數中初始化一個對象引用    

      b)將對象的引用保存到volatile類型的域或者AtomitReference對象中

      c)將對象的引用保存到某個正確構造對象的final類型域中

      d)將對象的引用保存到一個由鎖保護的域中

    2)線程安全庫中的容器提供了一下的安全發布保證:

      a)通過將一個鍵或者值放入Hashtable,SynchronizedMap或者ConscurrentMap中可以安全的將它發布給任何從這些容器中訪問的線程(無論是直接訪問還是通過迭代器訪問)。

      b)通過將某個元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,SynchronizedList或SynchronizedSet中,可以將該元素安全的發布到任何從這些容器中訪問該元素的線程。

      c)通過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以將該元素安全地發布到從這些隊列中訪問該元素的線程。

    3)通常要發布一個靜態構造的對象,最簡單和安全的方式是使用靜態的初始化器。靜態初始化器由JVM在類的初始化階段執行。由於在JVM內部存在著同步機制,因此通過這種方式初始化的任何對象都可以被安全地發布。

    2.事實不可變對象

     如果一個對象從技術上來看是可變的,但其狀態在發布後不會再改變,那麼這種對象稱為“事實不可變對象”。在沒有額外的同步的情況下,任何線程收可以安全地使用被安全發布的事實不可變對象。

    3.可變對象

     對象的發布需求取決於它的可變性:

     1)不可變對象可以通過任意機制來發布

     2)事實不可變對象必須通過安全發布方式來發布

     3)可變對象必須通過安全方式來發布,並且必須是線程安全的或者由某個鎖保護起來

    4.安全共享對象

     在並發程序中使用和共享對象是可以使用一些實用的策略,包括:

     1)線程封閉:線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改

     2)只讀共享:在沒有額外天同步的情況下,共享的只讀對象可以由多個線程並發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象

     3)線程安全共享:線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來訪問為不需要進一步的同步

     4)保護對象:被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及發布的並且由某個特定鎖保護的對象。 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved