單例模式是一種經常使用的設計模式,該模式提供了一種建立對象的方法,確保在程序中一個類最多隻有一個實例。java
單例有什麼用處?設計模式
有一些對象其實咱們只須要一個,好比線程池、緩存、對話框、處理偏好設置和註冊表的對象、日誌對象,充當打印機、顯示等設備的驅動程序對象。其實,這類對象只能有一個實例,若是製造出來多個實例,就會致使許多問題,如:程序的行爲異常、資源使用過量,或者是不一致的結果。緩存
Singleton一般用來表明那些本質上惟一的系統組件,好比窗口管理器或者文件系統。安全
在Java中實現單例模式,須要一個靜態變量、一個靜態方法和私有的構造器。多線程
對於一個簡單的單例模式,能夠這樣實現:併發
定義私有的構造方法。這樣別處的代碼沒法經過調用該類的構造函數來實例化該類的對象,只能經過該類提供的靜態方法來獲得該類的惟一實例;函數
提供一個getInstance()方法,該方法中判斷是否已經存在該類的實例,若是存在直接返回,不存在則新建一個再返回。代碼以下:post
public class Singleton{ private static Singleton uniqueInstance;//私有靜態變量 //私有的構造器。這樣別處的代碼沒法經過調用該類的構造函數來實例化該類的對象,只能經過該類提供的靜態方法來獲得該類的惟一實例。 private Singleton(){} //靜態方法 public static Singleton getInstance(){ //若是不存在,利用私有構造器產生一個Singleton實例並賦值到uniqueInstance靜態變量中。 //若是咱們不須要這個實例,他就永遠不會產生。這叫作「延遲實例化(懶加載)「 if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; } }
這段代碼使用了延遲實例化,在單線程中沒有任何問題。可是在多線程環境下,當有多個線程並行調用 getInstance(),都認爲uniqueInstance爲null的時候,就會調用uniqueInstance = new Singleton();
,這樣就會建立多個Singleton實例,沒法保證單例。性能
解決多線程環境下的線程安全問題,主要有如下幾種寫法:線程
關鍵字synchronized能夠保證在他同一時刻,只有一個線程能夠執行某一個方法,或者某一個代碼塊。
同步getInstance()方法是處理多線程最直接的作法。只要把getInstance()變成同步(synchronized)方法,就能夠解決併發問題了。
public class Singleton{ private static Singleton uniqueInstance;//私有靜態變量 //私有構造器 private Singleton() {} //synchronized同步方法 public static synchronized Singleton getInstance(){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; } }
可是,同步的效率低,會下降性能。只有第一次執行此方法的時候,才真正須要同步。也就是說,一旦設置好uniqueInstance變量,就再也不須要同步這個方法了。以後每次調用這個方法,同步都是一種累贅。同步getInstance()方法既簡單又有效。若是說對性能要求不高,這樣就能夠知足要求。
以前的實現採用的是懶加載方式,也就是說,當真正用到的時候纔會建立;若是沒被使用到,就一直不會建立。
懶加載方式在第一次使用的時候, 須要進行初始化操做,可能會比較耗時。
若是肯定一個對象必定會使用的話,能夠採用「急切」地實例化,事先準備好這個對象,須要的時候直接使用就好了。這種方式也叫作餓漢模式。具體代碼:
public class Singleton{ //在靜態初始化器中建立單例,保證了線程安全性 private static Singleton uniqueInstance = new Singleton(); private Singleton() {} public static Singleton getInstance(){ return uniqueInstance; } }
餓漢模式是如何保證線程安全的?
餓漢模式中的靜態變量是隨着類加載時被初始化的。static關鍵字保證了該變量是類級別的,也就是說這個類被加載的時候被初始化一次。注意與對象級別和方法級別進行區分。
由於類的初始化是由類加載器完成的,這實際上是利用了類加載器的線程安全機制。類加載器的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是由於這樣, 除非被重寫,這個方法默認在整個裝載過程當中都是同步的(線程安全的)。
殺雞用牛刀。實現單例模式能夠利用雙重檢查加鎖(double-checked locking),首先檢查是否實例已經建立了,若是還沒有建立,「才」進行同步。這樣,只有第一次會同步。
public class Singleton{ //使用volatile關鍵字,確保當uniqueInstance變量被初始化成爲Singleton實例時,多線程能夠正確地處理uniqueInstance變量。 private volatile static Singleton uniqueInstance; private Singleton() {} public static Singleton getInstance() { if(uniqueInstance == null){//第一次檢查 synchronized(Singleton.class){ if(uniqueInstance == null){//第二次檢查 uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
若是性能是關注的重點,雙重檢查加鎖能夠大幅減小getInstance()的時間消耗成本。
在Java 1.5發行版本以前,雙重檢查模式的功能很不穩定,由於volatile修飾符的語義不夠強,難以支持它。Java 1.5發行版本中引入的內存模式解決了這個問題,現在,雙重檢查模式是延遲初始化的一個實例域的方法。
爲何要進行雙重檢查?只檢查一次不行嗎?
解答:只檢查一次不行。只檢查一次的代碼以下:
if(uniqueInstance == null){//第一次檢查 synchronized(Singleton.class){ uniqueInstance = new Singleton(); } }
當兩個線程同時判斷uniqueInstance == null的時候,都會去得到Singleton.class的鎖對象,因爲兩個線程擁有的鎖對象是同一個Singleton.class,兩個線程前後執行,也就是兩個線程都會進入同步代碼塊建立一個新的對象,形成返回的uniqueInstance 並非惟一的,這樣也就不符合單例模式了。
從Java 1.5發行版本起,實現Singleton只須要編寫一個包含單個元素的枚舉類型:
public enum Singleton { INSTANCE; }
使用枚舉實現單例的方法雖然尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。注意:若是Singleton必須拓展一個超類,而不是擴展Enum的時候,則不宜使用這個方法。