多線程之 Final變量 詳解

原文:java

http://www.tuicool.com/articles/2Yjmqy編程

併發編程網:http://ifeve.com/java-memory-model/緩存

 

總結:安全

Final 變量在併發當中,原理是經過禁止cpu的指令集重排序(重排序詳解http://ifeve.com/java-memory-model-1/ http://ifeve.com/java-memory-model-2/),來提供現成的課件性,來保證對象的安全發佈,防止對象引用被其餘線程在對象被徹底構造完成前拿到並使用。多線程

 

與前面介紹的鎖和volatile相比較,對final域的讀和寫更像是普通的變量訪問。對於final域,編譯器和處理器要遵照兩個重排序規則:併發

 

  1. 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
  2. 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。

 

 

與Volatile 有類似做用,不過Final主要用於不可變變量(基本數據類型和非基本數據類型),進行安全的發佈(初始化)。而Volatile能夠用於安全的發佈不可變變量,也能夠提供可變變量的可見性。
函數

 

 


 

安全發佈的經常使用模式

可變對象必須經過安全的方式來發布,這一般意味着在發佈和使用該對象的線程時都必須使用同步。如今,咱們將重點介紹如何確保使用對象的線程可以看到該對象處於已發佈的狀態,並稍後介紹如何在對象發佈後對其可見性進行修改。性能

安全地發佈一個對象,對象的應用以及對象的狀態必須同時對其餘線程可見。一個正確構造的對象能夠經過如下方式來安全地發佈:ui

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

在線程安全容器內部的同步意味着,在將對象放入到某個容器,例如Vector或synchronizedList時,將知足上述最後一條需求。若是線程A將對象X放入一個線程安全的容器,隨後線程B讀取這個對象,那麼能夠確保B看到A設置的X狀態,即使在這段讀/寫X的應用程序代碼中沒有包含顯式的同步。儘管Javadoc在這個主題上沒有給出很清晰的說明,但線程安全庫中的容器類提供瞭如下的安全發佈保證:this

  • 經過將一個鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,能夠安全地將它發佈給任何從這些容器中訪問它的線程(不管是直接訪問仍是經過迭代器訪問)
  • 經過將某個元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,能夠將該元素安全地發佈到任何從這些容器中訪問該元素的線程
  • 經過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,能夠將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。

類庫中的其餘數據傳遞機制(例如Future和Exchanger)一樣能實現安全發佈,在介紹這些機制時將討論它們的安全發佈功能。

一般,要發佈一個靜態構造的對象,最簡單和最安全的方式是使用靜態的初始化器: public static Holder holder = new Holder(42);

靜態初始化器由JVM在類的初始化階段執行。因爲在JVM內部存在着同步機制,所以經過這種方式初始化的任何對象均可以被安全地發佈[JLS 12.4.2]。

 


 

 

詳解以下:

1、不變性

知足同步需求的另外一種方法是使用不可變對象(Immutable Object)。到目前爲止,咱們介紹了許多與原子性和可見性相關的問題,例如獲得失效數據,丟失更新操做或光查到某個對象處於不一致的狀態等等,都與多線程視圖同時訪問同一個可變的狀態相關。若是對象的狀態不會改變,那麼這些問題與複雜性也就天然消失了。

若是某個對象在被建立後其狀態就不能被修改,那麼這個對象就被成爲不可變對象。線程安全型是不可變對象的固有屬性之一,他們的不變性條件是由 構造函數建立的,只要他們的狀態不改變,那麼這些不變性條件就能得以維持。

不可變對象很簡單。他們只有一種狀態,而且該 狀態由構造函數來控制 。在程序設計中一個最困難的地方就是判斷複雜對象的可能狀態。然而,判斷不可變對象的狀態卻很簡單。

雖然在 Java 規範和 Java 內存模型中都沒有給出不可變性的正式定義,但不可變性並不等於將對象中全部的域都聲明爲 final 類型,即便對象中全部的域都是 final 類型的,這個對象也仍然是可變的,由於在 final 類型的域中能夠保存對可變對象的引用。

當知足如下條件時,對象纔是不可變的:

  • 對象建立完以後其狀態就不能修改
  • 對象的全部與都是 final 類型
  • 對象時正確建立的(建立期間沒有 this 的逸出)

咱們來分析下面這個類。

@Immutable
public final class ThreeStooges {  private final Set<String> stooges = new HashSet<String>();  public ThreeStooges() {   stooges.add("Moe");   stooges.add("Larry");   stooges.add("Curly");  }  public boolean isStooge(String name) {   return stooges.contains(name);  } }

在不可變對象的內部仍可使用可變對象來管理它們的狀態,如 ThreeStooges 所示。儘管保存姓名的Set對象是可變的,但從ThreeStooges的設計中能夠看到,在Set對象構造完成後沒法對其進行修改。stooges是一個final類型的引用變量,所以全部的對象狀態都經過一個final域來訪問。最後一個要求是「正確地構造對象」,這個要求很容易知足,由於構造函數能使該引用由除了構造函數及其調用者以外的代碼來訪問。

因爲程序的狀態總在不斷地變化,你可能會認爲須要使用不可變對象的地方很少,但實際狀況並不是如此。在「不可變的對象」與「不可變的對象引用」之間存在着差別。保存在不可變對象中的程序狀態仍然能夠更新,即經過將一個保存新狀態的實例來「替換」原有的不可變對象。

Final 域

關鍵字 final 能夠視爲 C++ 中 const 機制的一種受限版本,用於構造不可變對象。final 類型的域是不能修改的(但若是 final 域所引用的對象時可變的,那麼這些被引用的對象是能夠修改的)。然而,在 Java 內存模型中,final 域還有着特殊的語義。final 域能確保初始化過程的安全性,從而能夠不受限制的訪問不可變對象,並在共享這些對象時無需同步。

注: 我的理解爲,final 字段一旦被初始化完成,而且構造器沒有把 this 引用傳遞出去,那麼在其餘線程中就能看到 final 字段的值(域內變量可見性,和 volatile 相似),並且其外部可見狀態永遠也不會改變。它所帶來的安全性是最簡單最純粹的。

注: 即便對象是可變的,經過將對象的某些域聲明爲final類型,仍然能夠 簡化對狀態的判斷 ,所以限制對象的可變性也就至關於限制了該對象可能的狀態集合。僅包含一個或兩個可變狀態的「基本不可變」對象仍然比包含多個可變狀態的對象簡單。經過將域聲明爲final類型,也至關於告訴維護人員這些域是不會變化的。

正如「除非須要更高的可見性,不然應將全部的餓域都聲明爲私有域」[EJ Item 12]是一個良好的變成習慣,「除非須要某個域是可變的,不然應將其聲明爲final域」也是一個良好的變成習慣。

示例:使用 Volatile 類型來發布不可變對象

以前咱們講過, volatile 能夠用來保證域的可見性而不能保證變量操做的原子性,更爲準確的講,只能保證讀寫操做具備原子性,而不能保證自增 i++ 等運算操做的原子性。

在前面的UnsafeCachingFactorizer類中,咱們嘗試用兩個AtomicReferences變量來保存最新的數值及其因數分解結果,但這種方式並不是是線程安全的,由於咱們沒法以原子方式來同時讀取或更新這兩個相關的值。一樣,用volatile類型的變量來保存這些值也不是線程安全的。然而,在某些狀況下,不可變對象能提供一種弱形式的原子性。

因式分解Servlet將執行兩個原子操做:更新緩存的結果,以及經過判斷緩存中的數值是否等於請求的數值來決定是否直接讀取緩存中的因數分解結果。每當須要對一組相關數據以原子方式執行某個操做時,就能夠考慮建立一個不可變的類來包含這些數據,例如 OneValueCache。

@Immutable
class OneValueCache {  private final BigInteger lastNumber;  private final BigInteger[] lastFactors;  /**  * 若是在構造函數中沒有使用 Arrays.copyOf()方法,那麼域內不可變對象 lastFactors卻能被域外代碼改變  * 那麼 OneValueCache 就不是不可變的。  */  public OneValueCache(BigInteger i,       BigInteger[] factors) {   lastNumber = i;   lastFactors = Arrays.copyOf(factors, factors.length);  }  public BigInteger[] getFactors(BigInteger i) {   if (lastNumber == null || !lastNumber.equals(i))    return null;   else    return Arrays.copyOf(lastFactors, lastFactors.length);  } }
        對於在訪問和更新多個相關變量時出現的競爭條件問題,能夠經過將這些變量所有保存在一個不可變對象中來消除。若是是一個可變的對象,那麼就必須使用鎖來確保原子性。若是是一個不可變對象,那麼當線程得到了對該對象的引用後,就  沒必要擔憂另外一個線程會修改對象的狀態

。若是要更新這些變量,那麼能夠建立一個新的容器對象,但其餘使用原有對象的線程仍然會看到對象處於一致的狀態。

在 VolatileCachedFactorizer使用了OneValueCache來保存緩存的數值及其因數。咱們將 OneValueCache 聲明爲 volatile,這樣當一個線程將cache設置爲引用一個新的OneValueCache時,其餘線程就會當即看到新緩存的數據。

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {  private volatile OneValueCache cache =   new OneValueCache(null, null);  public void service(ServletRequest req, ServletResponse resp) {   BigInteger i = extractFromRequest(req);   BigInteger[] factors = cache.getFactors(i);   if (factors == null) {    factors = factor(i);    cache = new OneValueCache(i, factors);//聲明爲 volatile ,防止指令重排序,保證可見性   }   encodeIntoResponse(resp, factors);  } }

與cache相關的操做不會相互干擾,由於OneValueCache是不可變的,而且在每條相應的代碼路徑中只會訪問它一次。經過使用包含多個狀態變量的容器對象來維持不變性條件,並使用一個volatile類型的引用來確保可見性,使得Volatile Cached Factorizer在沒有顯式地使用鎖的狀況下仍然是線程安全的。

2、安全發佈

到目前爲止,咱們重點討論的是如何確保對象不被髮布,例如讓對象封閉在線程或另外一個對象的內部。固然,在某些狀況下咱們但願在多個線程間共享對象,此時必須確保安全地進行共享。然而,若是隻是像下面程序那樣將對象引用保存到公有域中,那麼還不足以安全地發佈這個對象。

//不安全的發佈
public Holder holder; public void initialize() { holder = new Holder(42); }

你可能會奇怪,這個看似沒有問題的示例何以會運行失敗。因爲存在可見性問題,其餘線程看到的Holder對象將處於不一致的狀態,即使在該對象的構造函數中已經正確地構建了不變性條件。這種不正確的發佈致使其餘線程看到還沒有建立完成的對象。

不正確的發佈:正確的對象被破壞

你不能期望一個還沒有被徹底建立的對象擁有完整性。某個觀察該對象的線程將看到對象處於不一致的狀態,而後看到對象的狀態忽然發生變化,即便線程在對象發佈後尚未修改過它。事實上,若是下面程序中的Holder使用前面程序中的不安全發佈方式,那麼另外一個線程在調用assertSanity時將拋出AssertionError。

public class Holder {  private int n;  public Holder(int n) { this.n = n; }  public void assertSanity() {   if (n != n)    throw new AssertionError("This statement is false.");  } }

因爲沒有使用同步來確保Holder對象對其餘線程可見,所以將Holder稱爲「未被正確發佈」。在未被正確發佈的對象中存在兩個問題。

首先 ,除了發佈對象的線程外,其餘線程能夠看到的 Holder域是一個失效值 ,所以將看到一個空引用或者以前的舊值。

 然而 ,更糟糕的狀況是,線程看到Holder引用的值是最新的,但Holder狀態的值倒是失效的。狀況變得更加不可預測的是,某個線程在第一次讀取域時獲得失效值,而再次讀取這個域時會獲得一個更新值,這也是assertSainty拋出AssertionError的緣由。

若是沒有足夠的同步,那麼當在多個線程間共享數據時將發生一些很是奇怪的事情。

不可變對象與初始化安全性

因爲不可變對象是一種很是重要的對象,所以 Java內存模型爲不可變對象的共享提供了一種特殊的初始化安全性保證 。咱們已經知道,即便某個對象的引用對其餘線程是可見的,也並不意味着對象狀態對於使用該對象的線程來講必定是可見的。爲了確保對象狀態能呈現出一致的視圖,就必須使用同步。

另外一方面,即便在發佈不可變對象的引用時沒有使用同步,也仍然能夠安全地訪問該對象。爲了維持這種初始化安全性的保證,必須知足不可變性的全部需求:狀態不可修改,全部域都是final類型,以及正確的構造過程。( 若是Holder對象是不可變的,那麼即便Holder沒有被正確地發佈,在assertSanity中也不會拋出AssertionError。)

任何線程均可以在不須要額外同步的狀況下安全地訪問不可改變對象,即便在發佈這些對象時沒有使用同步。

這種保證還將延伸到被正確建立對象中全部final類型的域。在沒有額外同步的狀況下,也能夠安全地訪問final類型的域。然而,若是final類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍然須要同步。

安全發佈的經常使用模式

可變對象必須經過安全的方式來發布,這一般意味着在發佈和使用該對象的線程時都必須使用同步。如今,咱們將重點介紹如何確保使用對象的線程可以看到該對象處於已發佈的狀態,並稍後介紹如何在對象發佈後對其可見性進行修改。

安全地發佈一個對象,對象的應用以及對象的狀態必須同時對其餘線程可見。一個正確構造的對象能夠經過如下方式來安全地發佈:

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

在線程安全容器內部的同步意味着,在將對象放入到某個容器,例如Vector或synchronizedList時,將知足上述最後一條需求。若是線程A將對象X放入一個線程安全的容器,隨後線程B讀取這個對象,那麼能夠確保B看到A設置的X狀態,即使在這段讀/寫X的應用程序代碼中沒有包含顯式的同步。儘管Javadoc在這個主題上沒有給出很清晰的說明,但線程安全庫中的容器類提供瞭如下的安全發佈保證:

  • 經過將一個鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,能夠安全地將它發佈給任何從這些容器中訪問它的線程(不管是直接訪問仍是經過迭代器訪問)
  • 經過將某個元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,能夠將該元素安全地發佈到任何從這些容器中訪問該元素的線程
  • 經過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,能夠將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。

類庫中的其餘數據傳遞機制(例如Future和Exchanger)一樣能實現安全發佈,在介紹這些機制時將討論它們的安全發佈功能。

一般,要發佈一個靜態構造的對象,最簡單和最安全的方式是使用靜態的初始化器: public static Holder holder = new Holder(42);

靜態初始化器由JVM在類的初始化階段執行。因爲在JVM內部存在着同步機制,所以經過這種方式初始化的任何對象均可以被安全地發佈[JLS 12.4.2]。

事實不可變對象

若是對象在發佈後不會被修改,那麼對於其餘在沒有額外同步的狀況下安全地訪問這些對象的線程來講,安全發佈是足夠的。全部的安全發佈機制都能確保,當對象的引用對全部訪問該對象的線程可見時,對象發佈時的狀態對於全部線程也將是可見的,而且若是對象狀態不會再改變,那麼就足以確保任何訪問都是安全的。

若是對象從技術上來看是可變的,但其狀態在發佈後不會再改變,那麼把這種對象稱爲「 事實不可變對象 (Effectively Immutable Object)」。這些對象不須要知足以前提出的不可變性的嚴格定義。在這些對象發佈後,程序只需將它們視爲不可變對象便可。經過使用事實不可變對象,不只能夠簡化開發過程,並且還能因爲減小了同步而提升性能。

在沒有額外的同步的狀況下,任何線程均可以安全地使用被安全發佈的事實不可變對象。

例如,Date自己是可變的,但若是將它做爲不可變對象來使用,那麼在多個線程之間共享Date對象時,就能夠省去對鎖的使用。假設須要維護一個Map對象,其中保存了每位用戶的最近登陸時間: public Map<String, Date> lastLogin =Collections.synchronizedMap(new HashMap<String, Date>());

若是Date對象的值在被放入Map後就不會改變,那麼synchronizedMap中的同步機制就足以使Date值被安全地發佈,而且在訪問這些Date值時不須要額外的同步。

可變對象

若是對象在構造後能夠修改,那麼安全發佈只能確保「發佈當時」狀態的可見性。對於可變對象,不只在發佈對象時須要使用同步,並且在每次對象訪問時一樣須要使用同步來確保後續修改操做的可見性。要安全地共享可變對象,這些對象就必須被安全地發佈,而且必須是線程安全的或者由某個鎖保護起來。

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

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

安全的共享對象

當得到對象的一個引用時,你須要知道在這個引用上能夠執行哪些操做。在使用它以前是否須要得到一個鎖?是否能夠修改它的狀態,或者只能讀取它?許多併發錯誤都是因爲沒有理解共享對象的這些「既定規則」而致使的。當發佈一個對象時,必須明確地說明對象的訪問方式。

相關文章
相關標籤/搜索