java設計模式 -- 單例模式

mmexport1520334289708.gif

閱讀原文

在介紹單例模式之前,我們先了解一下,什么是設計模式?
設計模式(Design Pattern):是一套被反復使用,多數人知曉的,經過分類編目的,代碼設計經驗的總結。
目的:使用設計模式是為了可重用性代碼,讓代碼更容易被他人理解,保證代碼可靠性。

本文將會用到的關鍵詞:

  • 單例:Singleton
  • 實例:instance
  • 同步:synchronized
  • 類裝載器:ClassLoader

單例模式

單例,顧名思義就是只能有一個、不能再出現第二個。就如同地球上沒有兩片一模一樣的樹葉一樣。

在這里就是說:一個類只能有一個實例,并且整個項目系統都能訪問該實例。

單例模式共分為兩大類:

  • 懶漢模式:實例在第一次使用時創建
  • 餓漢模式:實例在類裝載時創建

單例模式UML圖

單例模式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;
    private Singleton(){
    }
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); 
        }        
        return instance; 
   }
}

每次獲取instance之前先進行判斷,如果instance為空就new一個出來,否則就直接返回已存在的instance。這種寫法在大多數的時候也是沒問題的。問題在于,當多線程工作的時候,如果有多個線程同時運行到if (instance == null),都判斷為null,那么兩個線程就各自會創建一個實例——這樣一來,就不是單例了。
這時我們需要使用synchronized,加上一個同步鎖

// 代碼二
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        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;
    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),其實是為了解決Version2中的效率問題,只有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,了解了原子操作和指令重排的概念之后,我們再繼續看Version3代碼的問題。下面這段話直接從陳皓的文章(深入淺出單實例SINGLETON設計模式)中復制而來:主要在于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;
    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的最佳方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容