單例餓漢模式中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值時不需要額外的同步。
可變物件
如果物件在構造後可以修改,那麼安全釋出只能確保“釋出當時”狀態的可見性。對於可變物件,不僅在釋出物件時需要使用同步,而且在每次物件訪問時同樣需要使用同步來確保後續修改操作的可見性。要安全地共享可變物件,這些物件就必須被安全地釋出,並且必須是執行緒安全的或者由某個鎖保護起來。
物件的釋出需求取決於它的可變性:
- 不可變物件可以通過任意機制來發布
- 事實不可改變必須通過安全方式釋出
- 可變物件必須通過安全方式釋出,並且必須是執行緒安全的或者由某個鎖保護起來
安全的共享物件
當獲得物件的一個引用時,你需要知道在這個引用上可以執行哪些操作。在使用它之前是否需要獲得一個鎖?是否可以修改它的狀態,或者只能讀取它?許多併發錯誤都是由於沒有理解共享物件的這些“既定規則”而導致的。當釋出一個物件時,必須明確地說明物件的訪問方式。
相關推薦
單例餓漢模式中final關鍵字的作用
原文: http://www.tuicool.com/articles/2Yjmqy 併發程式設計網:http://ifeve.com/java-memory-model/ 總結: Final 變數在併發當中,原理是通過禁止cpu的指令集重排序(重排序詳解http://ifeve.com/java-mem
java單例餓漢和懶漢模式
面試時,經常會問到單例模式。 單例模式的兩種方式: 一種是餓漢式,就是在類初始化的時候,建立物件,這種方式是執行緒安全的,在程式執行期間就這一個物件。 另一種是懶漢式,懶漢式是在第一次使用時才建立物件,但是如果在多執行緒環境中要考慮執行緒安全問題。 比較喜歡的方
單例餓漢式和飽漢式各自的有缺點
單例模式應用於一個類只有一個例項的情況,並且為其例項提供一個全域性的訪問點。 特點: 1.一個類只有一個例項 2.自己建立這個例項 3.整個系統只能用這個例項 應用場景 外部資源:每臺計算機有若干個印表機,但只能有一個PrinterSpooler,以避免兩個列
Java 懶漢式單例 餓漢式單例
轉載請註明出處:http://blog.csdn.net/mr_liabill/article/details/48374921 來自《LiaBin的部落格》 單例模式很常見,在面試中也會經常直接讓你寫一個單例出來 單例模式寫法一般分為兩種,懶漢式和餓漢式 餓漢式
單例模式中懶漢模式與餓漢模式
/** * @author 萬星明 * @version 建立時間:2018年10月26日 下午4:32:10 * 請編寫一個單例模式,類名自己定義(不允許出現無意義命名)。 * 分別用懶
單例模式中,餓漢式和懶漢式有什麼區別?各適合用在哪裡?為什麼說推薦用餓漢模式?
餓漢式: public class Singleton{ private static Singleton singleton = new Singleton (); private Singleton (){} public
C++中的單例模式(懶漢模式、餓漢模式及執行緒安全問題)
1 教科書裡的單例模式 我們都很清楚一個簡單的單例模式該怎樣去實現:建構函式宣告為private或protect防止被外部函式例項化,內部儲存一個private static的類指標儲存唯一的例項,例項的動作由一個public的類方法代勞,該方法也返回單例類唯一的例
多線程單例模式之立即加載(餓漢模式)
run tel ext 相同 turn nbsp 加載 一個 nis package com.wz.thread.immediately;/** * 立即加載/餓漢模式 單例設計模式 * @author Administrator * */public class MyOb
第六章單例模式與多執行緒——立即載入“餓漢模式”與延遲載入“懶漢模式”
立即載入就是使用類的時候已經將物件建立完畢了,也稱為“餓漢模式” package test01; public class MyObject { // 建立物件 private static MyObject object = new MyObject(); private MyObjec
單例模式下的懶漢和餓漢模式
1 //單例模式---懶漢模式 2 public class Apple{ 3 //建立一個成員,在記憶體中只有一個拷貝 4 private static Apple apple = null; 5 private Apple(){ 6 7
單例模式、懶漢模式以及餓漢模式的區別及應用
1.單例模式 單例模式就是系統執行期間,有且僅有一個例項。它有三個必須滿足的關鍵點: (1)一個類只有一個例項。這是滿足單例模式最基本的要求,若滿足這個關鍵點,只能提供私有的構造器,即保證不能隨意建立該類的例項。示例如下: //讀取配置檔案的工具類—單例模式 public class Con
【C++】單例模式:餓漢模式和懶漢模式
餓漢模式:提前建立一個靜態的類物件,把所有能夠建立物件的模組全部私有化,從外部需要建立類物件時只能返回事先建立好的唯一物件。就像一個特別飢餓的人,提前準備好食物,只要餓了,就可以立刻食用。 /*惡漢模式--單例模式*/ #include<iostream> using namespa
單例模式---懶漢與餓漢模式和靜態內部類實現
單例模式是最基本的 java 設計模式之一 主要有兩種設計方法,主要分為餓漢式與懶漢式 餓漢式比較簡單,直接使用常量即可,主要程式碼如下: private static final SingleModel INSTANCE = new Sing
單例設計模式的設計——餓漢模式
package SingleInstanceModel; /** * Created by JYM on 2019/1/8 * 單例模式下的:餓漢模式 * */ //final不允許被繼承 public final class Singleton { //例
java 單例模式之執行緒安全的餓漢模式和懶漢模式
單例模式 解決的問題:保證一個類在記憶體中的物件唯一性. 比如:多程式讀取一個配置檔案時,建議配置檔案封裝成物件。會方便操作其中資料,又要保證多個程式讀到的是同一個配置檔案物件, 就需要該配置檔案物件在記憶體中是唯一的。 如何保證物件唯一性呢? 思想: 1,不讓其他程式建立
單例模式,餓漢模式和懶漢模式
什麼是單例?單例就是系統執行過程中有且僅有一個例項存在, 即:一個類只有一個例項--最基本的要求(只提供私有構造器),它必須自己建立這個例項(定義靜態的該類的私有物件),它必須自行向整個系統提供這個例項(提供一個靜態的公有方法,返回建立或者獲取本身的靜態私有物件); 單例模
單例模式(餓漢模式和懶漢模式)
單例模式也叫單件模式。Singleton是一個非常常用的設計模式,幾乎所有稍微大一些的程式都會使用到它,所以構建一個執行緒安全並且高效的Singleton很重要。 單例模式的特點: 1>單例類保證全域性只有一個唯一例項物件。 2>單例類提供獲取
單例模式——餓漢模式
所謂的單例模式,就是設計一個類,它在整個程式中只能有一個該類的例項存在,這就是單例模式。 C++實現單例模式的一般方法是將建構函式,拷貝建構函式以及賦值運算子函式宣告成private,從而禁止他人建立例項。否則如果上面三者不為私有,那麼他人就可以呼叫上面的三個函式來建立例項
單例模式(懶漢模式與餓漢模式)
1.單例模式: 應用場景:當系統中只需要一個物件就夠了,如工作管理員、古代皇帝、現代老婆 作用:保證在一個系統中有且只有一個例項 型別:餓漢模式、懶漢模式 2.餓漢模式: public class Singleton { //1.私有化
Java單例模式之懶漢模式及餓漢模式
單例模式 單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。 這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一