首先,先看個例子吧 上面的代碼,是典型的懶漢式單例模式,在單線程的情況下,完全是沒有問題的,但是在多線程的環境下,就很難保證對象的單例了。那應該如何去保證在多線程的環境下的單例呢?相信學過了,學過多線程的基本知識的都知道在可能會產生併發的地方,加上同步就好,即synchronized(同步方法或者同 ...
首先,先看個例子吧
1 public class SimpleSingleton 2 { 3 // 靜態屬性,屬於類 ,對於任何對象都是唯一的 4 private static SimpleSingleton simpleSingleton; 5 // 私有化構造方法,讓外部不可以隨意的創建對象 6 private SimpleSingleton() {}; 7 // 提供公共的方法,獲取唯一的實例對象 8 public static SimpleSingleton getInstance() 9 { 10 if(null == simpleSingleton) // 如果有多個線程同時都,通過了非null的判斷,就會出現多線程環境下的安全問題,就不會保證單例了 11 { 12 simpleSingleton = new SimpleSingleton(); 13 } 14 return simpleSingleton; 15 } 16 }
上面的代碼,是典型的懶漢式單例模式,在單線程的情況下,完全是沒有問題的,但是在多線程的環境下,就很難保證對象的單例了。那應該如何去保證在多線程的環境下的單例呢?相信學過了,學過多線程的基本知識的都知道在可能會產生併發的地方,加上同步就好,即synchronized(同步方法或者同步代碼塊)。基於這點基本的知識,那就改一改上面的代碼吧。
1 public class SimpleSingleton 2 { 3 // 靜態屬性,屬於類 ,對於任何對象都是唯一的 4 private static SimpleSingleton simpleSingleton; 5 // 私有化構造方法,讓外部不可以隨意的創建對象 6 private SimpleSingleton() {}; 7 // 提供公共的方法,獲取唯一的實例對象 8 public synchronized static SimpleSingleton getInstance() 9 { 10 if(null == simpleSingleton) 11 { 12 simpleSingleton = new SimpleSingleton(); 13 } 14 return simpleSingleton; 15 } 16 }
這樣,在getInstance的方法之上,加上synchronized就可以保證,在多線程環境下的單例了。看著好像沒什麼問題了,再看看呢?有沒有想過為什麼要用同步方法實現上述代碼,而沒有用同步代碼塊實現上邊的代碼?那同步方法和同步代碼塊有有什麼區別呢?一般來說,同步方法因為應用在方法上,同步涉及的範圍或者說作用域廣一點,那麼就意味著包括的代碼執行的就多,那消耗的時間就多,別忘了,在同步方法被鎖定以後,別的線程想要訪問該方法,就要一直苦苦的在門外守候,哎,這樣整個性能就被拉低了;而說到同步代碼塊,一般會放在方法內,相對管的區域就小一點,性能自然就好一點咯。看看就看出問題來了,太不經考驗了,來來接著改。註意,只要在“變”的地方加上同步代碼塊,也就是simpleSingleton = new SimpleSingleton(); 同步方法每次都要獲取鎖判斷一次,其實只是在第一次創建實例的時候真正起作用,同步代碼塊,可以避免這樣的消耗
1 public class SimpleSingleton 2 { 3 // 靜態屬性,屬於類 ,對於任何對象都是唯一的 4 private static SimpleSingleton simpleSingleton; 5 // 私有化構造方法,讓外部不可以隨意的創建對象 6 private SimpleSingleton() {}; 7 // 提供公共的方法,獲取唯一的實例對象 8 public static SimpleSingleton getInstance() 9 { 10 if(null == simpleSingleton) // 如果有多個線程同時都,通過了非null的判斷,就會出現多線程環境下的安全問題 11 { 12 synchronized (SimpleSingleton.class) 13 { 14 simpleSingleton = new SimpleSingleton(); 15 } 16 } 17 return simpleSingleton; 18 } 19 }
好了好了,這下該改好了吧,那回憶一下,第一段代碼看看是不是有類似的問題啊,現在假設有A,B兩個線程,都通過了(null == simpleSingleton)的判斷,如果A線程首先獲得了鎖,進入同步代碼塊,創建SimpleSingleton的實例,賦值給靜態變數simpleSingleton,釋放鎖,並返回實例;這時B線程也獲得了鎖,同樣再次創建了SimpleSingleton的實例,這樣也就出現了問題,單例的多線程安全再次被破壞。這裡就需要雙重加鎖(double kill)的機制了,繼續改:
1 public class SimpleSingleton 2 { 3 // 靜態屬性,屬於類 ,對於任何對象都是唯一的 4 private static SimpleSingleton simpleSingleton; 5 6 // 私有化構造方法,讓外部不可以隨意的創建對象 7 private SimpleSingleton() 8 { 9 } 10 11 // 提供公共的方法,獲取唯一的實例對象 12 public static SimpleSingleton getInstance() 13 { 14 if (null == simpleSingleton) 15 { 16 synchronized (SimpleSingleton.class) //1 17 { 18 if (null == simpleSingleton) //2 19 { 20 simpleSingleton = new SimpleSingleton();//3 21 } 22 } 23 } 24 return simpleSingleton; 25 } 26 }
如果涉及到JVM底層的時候,上述的雙重加鎖還是可能有問題的
雙重檢查鎖定背後的理論是完美的。不幸地是,現實完全不同。雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器電腦上順利運行。
雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺記憶體模型。記憶體模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。
我們應該明白一點,JVM在創建實例的看似是一部操作,其實是分成好幾步來完成的,也就是說從JVM的底層角度來說,創建對象並非原子性的操作過程,一般來說,雙重加鎖的單例模式不會有什麼問題,但也有萬一的可能。創建對象的步驟:1.分配記憶體空間 2.初始化構造器 3.將對象指向分配的記憶體地址 按照正常的邏輯,是沒有問題。但是由於JVM會對位元組碼進行調優,其中就有一條調優:調整指令的執行順序 如果2和3兩步驟顛倒就會出現,返回一個未完成初始化構造器的未完成初始化的對象
無序寫入(例證)
為解釋該問題,需要重新考察上述代碼段的 //3 行。此行代碼創建了一個 SimpleSingleton 對象並初始化變數 simpleSingleton 來引用此對象。這行代碼的問題是:在 SimpleSingleton構造函數體執行之前,變數 simpleSingleton可能成為非 null
的。
什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。假設上述代碼段執行以下事件序列:
- 線程 1 進入
getInstance()
方法。 - 由於 simpleSingleton 為
null
,線程 1 在 //1 處進入synchronized
塊。 - 線程 1 前進到 //3 處,但在構造函數執行之前,使實例成為非
null
。 - 線程 1 被線程 2 預占。
- 線程 2 檢查實例是否為
null
。因為實例不為 null,線程 2 將 simpleSingleton 引用返回給一個構造完整但部分初始化了的 SimpleSingleton對象。 - 線程 2 被線程 1 預占。
- 線程 1 通過運行 SimpleSingleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。
此事件序列發生線上程 2 返回一個尚未執行構造函數的對象的時候。
那有沒有好的解決方案呢?且聽我慢慢說來。。。。。。
方案一:就要說到了volatile這關鍵詞了,簡單講一講不過多涉及,上邊不是因為JVM優化導致陷入不可的錯誤中,如果對於屬性加上這個關鍵詞就表示此屬性不需要優化,也就不會出現創建對象創建一般的情況了,加了volatile就等於禁止JVM進行自動指令重排序優化,並且強制保證當前線程對於變數的任何寫入操作對於其他線程都是即時可見的,總之volatile會強行將對該變數的所有讀和取操作綁定成一個不可拆分的動作
1 public class SimpleSingleton 2 { 3 // 靜態屬性,屬於類 ,對於任何對象都是唯一的,添加volatile避免指令優化重排 4 private volatile static SimpleSingleton simpleSingleton; 5 6 // 私有化構造方法,讓外部不可以隨意的創建對象 7 private SimpleSingleton() 8 { 9 } 10 11 // 提供公共的方法,獲取唯一的實例對象,雙重加鎖機制 12 public static SimpleSingleton getInstance() 13 { 14 if (null == simpleSingleton) 15 { 16 synchronized (SimpleSingleton.class) 17 { 18 if (null == simpleSingleton) 19 { 20 simpleSingleton = new SimpleSingleton(); 21 } 22 } 23 } 24 return simpleSingleton; 25 } 26 }
註意:volatile關鍵字在JDK1.5及以後的版本才具有上述的意義
方案二:私有靜態內部類
1 public class InnerSingleton 2 { 3 //私有化構造方法 4 private InnerSingleton() {} 5 //提供公共的方法獲取實例 6 public static InnerSingleton getInstance() 7 { 8 return InnerSingletonInstance.innerSingleton; 9 } 10 //創建私有靜態內部類,以InnerSingleton的聲明靜態屬性 11 private static class InnerSingletonInstance 12 { 13 static InnerSingleton innerSingleton = new InnerSingleton(); 14 } 15 }
解釋一下:因為類的靜態屬性只有在第一次類載入的時候初始化,而且只有一次,所有可以保證是單例的,並且整個類初始化的過程是JVM保證整個過程是同步的,所以就不用擔心會因為中途終止,而出現上述只初始化一部分的情況。
方案三:餓漢式單例模式
1 public class HungrySingleton 2 { 3 //私有靜態屬性,類載入即初始化完成,且只有一次 4 private static HungrySingleton hungrySingleton = new HungrySingleton(); 5 //私有化構造方法 6 private HungrySingleton() 7 { 8 9 } 10 //提供公共的方法獲取實例 11 public static HungrySingleton getInstance() 12 { 13 return hungrySingleton; 14 } 15 }
單例模式幾點保證:
1.Singleton最多只有一個實例,在不考慮反射強行突破訪問限制的情況下。
2.保證了併發訪問的情況下,不會發生由於併發而產生多個實例。
3.保證了併發訪問的情況下,不會由於初始化動作未完全完成而造成使用了尚未正確初始化的實例。
本文參考:http://www.zuoxiaolong.com/blog/article.ftl?id=124
http://blog.csdn.net/chenchaofuck1/article/details/51702129
未完待續。。。。。。。。。。。。。。。。