多線程之 Final變量 詳解
原文:
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域,編譯器和處理器要遵守兩個重排序規則:
- 在構造函數内對一個final域的寫入,與随後把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序。
- 初次讀一個包含final域的對象的引用,與随後初次讀這個final域,這兩個操作之間不能重排序。
與Volatile 有相似作用,不過Final主要用于不可變變量(基本資料類型和非基本資料類型),進行安全的釋出(初始化)。而Volatile可以用于安全的釋出不可變變量,也可以提供可變變量的可見性。
安全釋出的常用模式
可變對象必須通過安全的方式來釋出,這通常意味着在釋出和使用該對象的線程時都必須使用同步。現在,我們将重點介紹如何確定使用對象的線程能夠看到該對象處于已釋出的狀态,并稍後介紹如何在對象釋出後對其可見性進行修改。
安全地釋出一個對象,對象的應用以及對象的狀态必須同時對其他線程可見。一個正确構造的對象可以通過以下方式來安全地釋出:
- 在靜态初始化函數中初始化一個對象引用
- 将對象的應用儲存到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]。
詳解如下:
一、不變性
滿足同步需求的另一種方法是使用不可變對象(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在沒有顯式地使用鎖的情況下仍然是線程安全的。
二、安全釋出
到目前為止,我們重點讨論的是如何確定對象不被釋出,例如讓對象封閉線上程或另一個對象的内部。當然,在某些情況下我們希望在多個線程間共享對象,此時必須確定安全地進行共享。然而,如果隻是像下面程式那樣将對象引用儲存到公有域中,那麼還不足以安全地釋出這個對象。
//不安全的釋出
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值時不需要額外的同步。
可變對象
如果對象在構造後可以修改,那麼安全釋出隻能確定“釋出當時”狀态的可見性。對于可變對象,不僅在釋出對象時需要使用同步,而且在每次對象通路時同樣需要使用同步來確定後續修改操作的可見性。要安全地共享可變對象,這些對象就必須被安全地釋出,并且必須是線程安全的或者由某個鎖保護起來。
對象的釋出需求取決于它的可變性:
- 不可變對象可以通過任意機制來釋出
- 事實不可改變必須通過安全方式釋出
- 可變對象必須通過安全方式釋出,并且必須是線程安全的或者由某個鎖保護起來
安全的共享對象
當獲得對象的一個引用時,你需要知道在這個引用上可以執行哪些操作。在使用它之前是否需要獲得一個鎖?是否可以修改它的狀态,或者隻能讀取它?許多并發錯誤都是由于沒有了解共享對象的這些“既定規則”而導緻的。當釋出一個對象時,必須明确地說明對象的通路方式。