摘要:所以這段代碼也就避免了代碼一中,可能出現因為多線程導致多個實例的情況。從內部看是一個餓漢式的單例,但是從外部看來,又的確是懶漢式的實現枚舉使用是不是很簡單而且因為自動序列化機制,保證了線程的絕對安全。
在介紹單例模式之前,我們先了解一下,什么是設計模式?
設計模式(Design Pattern):是一套被反復使用,多數人知曉的,經過分類編目的,代碼設計經驗的總結。
目的:使用設計模式是為了可重用性代碼,讓代碼更容易被他人理解,保證代碼可靠性。
本文將會用到的關鍵詞:
單例:Singleton
實例:instance
同步:synchronized
類裝載器:ClassLoader
單例模式:
單例,顧名思義就是只能有一個、不能再出現第二個。就如同地球上沒有兩片一模一樣的樹葉一樣。
在這里就是說:一個類只能有一個實例,并且整個項目系統都能訪問該實例。
單例模式共分為兩大類:
懶漢模式:實例在第一次使用時創建
餓漢模式:實例在類裝載時創建
單例模式UML圖
按照定義我們可以寫出一個基本代碼:
public class Singleton { // 使用private將構造方法私有化,以防外界通過該構造方法創建多個實例 private Singleton() { } // 由于不能使用構造方法創建實例,所以需要在類的內部創建該類的唯一實例 // 使用static修飾singleton 在外界可以通過類名調用該實例 類名.成員名 static Singleton singleton = new Singleton(); // 1 // 如果使用private封裝該實例,則需要添加get方法實現對外界的開放 private static Singleton instance = new Singleton(); // 2 // 添加static,將該方法變成類所有 通過類名訪問 public static Singleton getInstance(){ return instance; } //1和2選一種即可,推薦2 }
對于餓漢模式來說,這種寫法已經很‘perfect’了,唯一的缺點就是,由于instance的初始化是在類加載時進行的,類加載是由ClassLoader來實現的,如果初始化太早,就會造成資源浪費。
當然,如果所需的單例占用的資源很少,并且也不依賴于其他數據,那么這種實現方式也是很好的。
new一個對象時
使用反射創建它的實例時
子類被加載時,如果父類還沒有加載,就先加載父類
JVM啟動時執行主類 會先被加載
懶漢模式懶漢模式的代碼如下
// 代碼一 public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
每次獲取instance之前先進行判斷,如果instance為空就new一個出來,否則就直接返回已存在的instance。
這種寫法在單線程的時候是沒問題的。但是,當有多個線程一起工作的時候,如果有兩個線程同時運行到 if (instance == null),都判斷為null(第一個線程判斷為空之后,并沒有繼續向下執行,當第二個線程判斷的時候instance依然為空),最終兩個線程就各自會創建一個實例出來。這樣就破環了單例模式 實例的唯一性
要想保證實例的唯一性就需要使用synchronized,加上一個同步鎖
// 代碼二 public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } return instance; } }
加上synchronized關鍵字之后,getInstance方法就會鎖上了。如果有兩個線程(T1、T2)同時執行到這個方法時,會有其中一個線程T1獲得同步鎖,得以繼續執行,而另一個線程T2則需要等待,當第T1執行完畢getInstance之后(完成了null判斷、對象創建、獲得返回值之后),T2線程才會執行執行。
所以這段代碼也就避免了代碼一中,可能出現因為多線程導致多個實例的情況。但是,這種寫法也有一個問題:給getInstance方法加鎖,雖然避免了可能會出現的多個實例問題,但是會強制除T1之外的所有線程等待,實際上會對程序的執行效率造成負面影響。
雙重檢查(Double-Check)代碼二相對于代碼一的效率問題,其實是為了解決1%幾率的問題,而使用了一個100%出現的防護盾。那有一個優化的思路,就是把100%出現的防護盾,也改為1%的幾率出現,使之只出現在可能會導致多個實例出現的地方。
代碼如下:
// 代碼三 public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null){ synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } } return instance; } }
這段代碼看起來有點復雜,注意其中有兩次if(instance==null)的判斷,這個叫做『雙重檢查 Double-Check』。
第一個 if(instance==null),其實是為了解決代碼二中的效率問題,只有instance為null的時候,才進入synchronized的代碼段大大減少了幾率。
第二個if(instance==null),則是跟代碼二一樣,是為了防止可能出現多個實例的情況。
這段代碼看起來已經完美無瑕了。當然,只是『看起來』,還是有小概率出現問題的。想要充分理解需要先弄清楚以下幾個概念:原子操作、指令重排。
原子操作
簡單來說,原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因為線程調度被打斷的操作。比如,簡單的賦值是一個原子操作:
m = 6; // 這是個原子操作
假如m原先的值為0,那么對于這個操作,要么執行成功m變成了6,要么是沒執行 m還是0,而不會出現諸如m=3這種中間態——即使是在并發的線程中。
但是,聲明并賦值就不是一個原子操作:
int n=6;//這不是一個原子操作
對于這個語句,至少有兩個操作:①聲明一個變量n ②給n賦值為6——這樣就會有一個中間狀態:變量n已經被聲明了但是還沒有被賦值的狀態。這樣,在多線程中,由于線程執行順序的不確定性,如果兩個線程都使用m,就可能會導致不穩定的結果出現。
指令重排
簡單來說,就是計算機為了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。比如,這一段代碼:
int a ; // 語句1 a = 8 ; // 語句2 int b = 9 ; // 語句3 int c = a + b ; // 語句4
正常來說,對于順序結構,執行的順序是自上到下,也即1234。但是,由于指令重排
的原因,因為不影響最終的結果,所以,實際執行的順序可能會變成3124或者1324。
由于語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排。——也就是說,對于非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序。
OK,了解了原子操作和指令重排的概念之后,我們再繼續看代碼三的問題。
主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
1. 給 singleton 分配內存
2. 調用 Singleton 的構造函數來初始化成員變量,形成實例
3. 將singleton對象指向分配的內存空間(執行完這步 singleton才是非 null了)
在JVM的即時編譯器中存在指令重排序的優化。
也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
再稍微解釋一下,就是說,由于有一個『instance已經不為null但是仍沒有完成初始化』的中間狀態,而這個時候,如果有其他線程剛好運行到第一層if (instance ==null)這里,這里讀取到的instance已經不為null了,所以就直接把這個中間狀態的instance拿去用了,就會產生問題。這里的關鍵在于線程T1對instance的寫操作沒有完成,線程T2就執行了讀操作。
對于代碼三出現的問題,解決方案為:給instance的聲明加上volatile關鍵字
代碼如下:
public class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null){ synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } } return instance; } }
volatile關鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內存屏障,這樣,在它的賦值完成之前,就不用會調用讀操作。
注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,不會調用讀操作(if (instance == null))。
其它方法 靜態內部類public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
這種寫法的巧妙之處在于:對于內部類SingletonHolder,它是一個餓漢式的單例實現,在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真單例。
同時,由于SingletonHolder是一個內部類,只在外部類的Singleton的getInstance()中被使用,所以它被加載的時機也就是在getInstance()方法第一次被調用的時候。
它利用了ClassLoader來保證了同步,同時又能讓開發者控制類加載的時機。從內部看是一個餓漢式的單例,但是從外部看來,又的確是懶漢式的實現
public enum SingleInstance { INSTANCE; public void fun1() { // do something } }// 使用SingleInstance.INSTANCE.fun1();
是不是很簡單?而且因為自動序列化機制,保證了線程的絕對安全。三個詞概括該方式:簡單、高效、安全
這種寫法在功能上與共有域方法相近,但是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即使是在面對復雜的序列化或者反射攻擊的時候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經成為實現Singleton的最佳方法。
原文地址
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/69226.html
摘要:使用靜態類體現的是基于對象,而使用單例設計模式體現的是面向對象。二編寫單例模式的代碼編寫單例模式的代碼其實很簡單,就分了三步將構造函數私有化在類的內部創建實例提供獲取唯一實例的方法餓漢式根據上面的步驟,我們就可以輕松完成創建單例對象了。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看...
摘要:如果需要防范這種攻擊,請修改構造函數,使其在被要求創建第二個實例時拋出異常。單例模式與單一職責原則有沖突。源碼地址參考文獻設計模式之禪 定義 單例模式是一個比較簡單的模式,其定義如下: 保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。 或者 Ensure a class has only one instance, and provide a global point of ac...
摘要:總結我們主要介紹到了以下幾種方式實現單例模式餓漢方式線程安全懶漢式非線程安全和關鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設計模式中文版第二版設計模式深入理解單例模式我是一個以架構師為年之內目標的小小白。 初遇設計模式在上個寒假,當時把每個設計模式過了一遍,對設計模式有了一個最初級的了解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。...
摘要:總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。二餓漢式單例餓漢式單例類在類初始化時,已經自行實例化靜態工廠方法餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以后不再改變,所以天生是線程安全的。 概念: Java中單例模式是一種常見的設計模式,單例模式的寫法有好幾種,這里主要介紹兩種:懶漢式單例、餓漢式單例。 單例模式有以下特點: 1、單例類只能有一個實例。 ...
摘要:一般來說,這種單例實現有兩種思路,私有構造器,枚舉。而這種方式又分了飽漢式,餓漢式。通過關鍵字防止指令重排序。什么是單例?為什么要用單例? 一個類被設計出來,就代表它表示具有某種行為(方法),屬性(成員變量),而一般情況下,當我們想使用這個類時,會使用new關鍵字,這時候jvm會幫我們構造一個該類的實例。而我們知道,對于new這個關鍵字以及該實例,相對而言是比較耗費資源的。所以如果我們能夠想...
閱讀 1633·2021-09-02 15:11
閱讀 1972·2019-08-30 14:04
閱讀 2558·2019-08-27 10:52
閱讀 1574·2019-08-26 11:52
閱讀 1196·2019-08-23 15:26
閱讀 2614·2019-08-23 15:09
閱讀 2603·2019-08-23 12:07
閱讀 2232·2019-08-22 18:41