# JAVA 併發編程—基礎


線程安全性

通過同步避免多個線程在同一時刻訪問相同的數據

1、如果當多個線程同時訪問一個可變的狀態變量時沒有使用合適的同步,那麼程序會出現錯誤。有三種方式可以修復這個問題:
(1)、不在多個線程之間共享該變量
(2)、將狀態變量改爲不可變的
(3)、在訪問狀態變量時使用同步

2、什麼是線程安全性?
在線程安全性的定義中,最主要的概念就是正確性。正確性意味着某個類的行爲與其規範完全一致。在良好的規範中通常會定義各種不變性條件來約束對象的狀態,以及定義各種後驗條件來描述對象操作的結果。
確定正確性的定義後,就可以定義線程安全性:當多個線程同時訪問某個類時,這個類始終能表現正確的行爲,那麼就稱這個類是線性安全的。

3、原子性、競態條件、複合操作
在併發編程中,如果對一個可變變量進行非原子的複合操作,會出現執行時序不正確的情況,發生競態條件,導致結果不正確。最常見的就是“先檢查後執行”操作。
在實際情況中,應該儘可能使用已有的線程安全對象,它們可能會對一些常用的複合操作進行了封裝。

4、內置鎖、重入
java提供了一種內置的鎖機制來支持原子性:同步代碼塊。包括兩部分:一個作爲鎖的對象引用,一個作爲這個鎖保護的代碼塊。以關鍵字synchronized修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象。
每個Java對象都可以用作一個實現同步的鎖,稱爲內置鎖監視器鎖。線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖。內置鎖相當於一個互斥體,意味着最多只能有一個線程持有這種鎖,即每次只能有一個線程執行內置鎖保護的代碼塊。
如果某個線程試圖獲得一個已經由它自己持有的鎖,那麼這個請求就會成功。“重入”意味着獲取鎖的操作粒度是“線程”,而不是“調用”。

5、用鎖來保護狀態
對於可能被多個線程同時訪問的可變狀態變量,在訪問它時(不僅僅是寫入共享變量)都要持有同一個鎖,在這種情況下,我們稱狀態變量是由這個鎖保護的。
一種常見的枷鎖約定是,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有訪問可變狀態的代碼路徑進行同步,使得在該對象上不會發生併發訪問。這種模式並無特殊之處,編譯器或運行時都不會強制實施這種模式。
並非所有數據都需要鎖的保護,只有被多個線程同時訪問的可變數據才需要通過鎖來保護。
當類的不變性條件涉及多個狀態變量時,那麼還有另一個需求:在不變性條件中的每個變量都必須由同一個鎖來保護。
爲什麼不在每個方法聲明時都使用關鍵字synchronized?雖然synchronized方法可以確保單個操作的原子性,但如果要把多個操作合併爲一個複合操作,還需要額外的枷鎖機制。此外,濫用同步方法會導致活躍性問題性能問題

6、活躍性與性能
synchronized意味着每次只有一個線程可以執行,當遇到執行時間較長的操作時,其它線程必須等待,會導致性能問題。幸運的是,可以通過縮小同步代碼塊的作用範圍,在確保併發性的同時又維護線程安全性。應該儘量將不影響共享狀態且執行時間較長的操作移出代碼塊。

對象的共享

如何共享和發佈對象,使它們能夠安全地由多個線程同時訪問。
同步還有另一個重要的方面:內存可見性。我們不僅希望防止某個線程正在使用對象狀態而另一個線程同時在修改該狀態,而且希望確保當一個線程修改了對象狀態後,其他線程能夠看到發生的狀態變化。

1、可見性、volatile變量
在沒有同步的情況下,編譯器、處理器以及運行時等都有可能對操作的執行順序進行一些重排序。
只要有數據在多個線程之間共享,就使用正確的同步。
非volatile類型的64爲數值變量可能會讀取到某個值的高32位和另一個值的低32位。
加鎖的含義不僅僅侷限於互斥行爲,還包括內存可見性。爲了確保所有線程都能看到共享變量的最新值,所有執行讀操作或者寫操作的線程都必須在同一個鎖上同步。
當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile變量時總會返回最新寫入的值。volatile變量只能確保可見性。
當且僅當滿足一下所有條件時,才應該使用volatile變量:

  • 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
  • 該變量不會與其它狀態變量一起納入不變性條件中。
  • 在訪問變量時不需要加鎖。

2、發佈與逸出
“發佈”一個對象的意思是指,使該對象能夠在當前作用域之外的代碼中使用。例如,將一個指向該對象的引用保存到其他代碼可以訪問的地方,或者將引用傳遞到其他類的方法中。當某個不該發佈的對象被髮布時,這種情況就被稱爲逸出
當發佈一個對象時,可能會間接發佈其他對象,比如List,Map等。
不要在構造過程中使this引用逸出。

3、線程封閉
一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不需要同步。這種技術被稱爲線程封閉,它是實現線程安全性的最簡單方式之一。線程封閉是程序設計中的一個考慮因素,必須在程序中實現。
Ad-hoc線程封閉是指,維護線程封閉性的職責完全由程序實現來承擔。這種方式是非常脆弱的,應該儘量少使用它。
棧封閉是線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象。在維持對象引用的棧封閉時,程序員需要多做一些工作以確保被引用的對象不會移除。
ThreadLocal類是一種更規範的方法,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal對象通常用於防止對可變的單實例變量或全局變量進行共享。要避免濫用ThreadLocal。

4、不變性
滿足同步需求的另一種方法是使用不可變對象。不可變對象一定是線程安全的。當滿足以下條件時,對象纔是不可變的:

  • 對象創建以後其狀態就不能修改
  • 對象的所有域必須是final類型(從技術上看,不可變對象並不需要將其所有的域都聲明爲final,要對類的良性數據競爭情況做精確分析,因此需要深入理解Java內存模型)
  • 對象是正確創建的(在對象的創建期間,this引用沒有逸出)

5、安全發佈、事實不可變對象
在未被正確發佈的對象中存在兩個問題。首先,除了發佈對象的線程外,其它線程可以看到的Holder域是一個失效值,因此將看到一個空引用或者之前的舊值。然而,更糟糕的是,線程看到Holder引用的值是最新的,但Holder狀態的值卻是失效的。
由於不可變對象是一種非常重要的對象,因此Java內存模型爲不可變對象的共享提供了一種特殊的初始化安全性保證。
可變對象必須通過安全的方式來發布,這通常意味這在發佈和使用該對象的線程時都必須使用同步。要安全的發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確構造的對象可以通過以下方法來安全地發佈:

  • 在靜態初始化函數中初始化一個對象引用
  • 將對象的引用保存到volatile類型的域或者AtomicReference容器中。
  • 將對象的引用保存到某個正確構造對象的final類型域中。
  • 將對象的引用保存到一個由鎖保護的域中。

如果對象從技術上來看是可變的,但其狀態在發佈後就不會再改變,那麼把這種對象稱爲“事實不可變對象”。在沒有額外的同步的情況下,任何線程都可以安全地使用被安全發佈的事實不可變變量。
可變對象不僅在發佈對象時需要使用同步,而且在每次對向訪問時同樣需要使用同步來確保後續修改的可見性。
對象的發佈需求取決於它的可變性:

  • 不可變對象可以通過任意機制來發布
  • 事實不可變對象必須通過安全方式來發布
  • 可變對象必須通過安全方式來發布,並且必須是線程安全的或者是由某個鎖保護起來

在併發線程中安全地共享對象,可以使用一些使用策略,包括:線程封閉只讀共享線程安全共享保護對象

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章