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

來源:互聯網
上載者:User

並發編程初探-對象的共用,並發編程初探對象
對象的共用   一、可見度

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

    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)保護對象:被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他安全執行緒對象中的對象,以及發布的並且由某個特定鎖保護的對象。 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.