今天咱們再來探討一下單例設計模式,能夠說,單例設計模式在面試考察中是最常出現的,單例模式看似簡單,每一個人可能均可以寫出來,可是能不能寫好就是一個問題,往深了考察,又能考察出面試者對於併發、類加載、序列化的掌握程度。java
單例有多種寫法,可是哪一種寫法更好的,爲何好呢,這些問題咱們都會在今天的文中一一解讀,首先咱們須要知道什麼是單例模式。面試
單例模式指的是,保證一個類只有一個實例,而且提供一個全局能夠訪問的入口。數據庫
其中第一個理由就是節省內存、節省計算。在咱們平時的程序中,咱們不少時候就只須要一個實例就夠了,若是出現了更多的實例反而屬於浪費。設計模式
舉個例子,咱們就拿一個初始化比較耗時的類來講,在這個類構造的時候,須要查詢數據庫,並對查到的數據作大量的計算,因此在第一次構造的時候,咱們花了不少時間來初始化這個對象,假設咱們數據庫裏的數據是不變的,而且把這個對象保存在了內存中,那麼之後就可使用同一個實例了,若是每次生成新的實例就沒有必要了。安全
第二個理由就是爲了保證結果的正確,比咱們須要一個全局的計數器,用來統計人數,若是有多個實例,反而可能會形成混亂。多線程
第三個理由就是方便管理,不少工具類咱們只須要一個實例,咱們經過一個統一的入口,好比經過getInstance方法,就能夠獲取到這個單例,這是很方便的,太多的實例不但沒有幫助,反而會顯得有點混亂。併發
無狀態工具類:如日誌工具類、字符串工具類...ide
全局信息類:如全局計數、環境變量類...函數
常見單例模式的寫法:工具
主要有五種:餓漢式、懶漢式、雙重檢查式、靜態內部類式、枚舉式。接下來根據難度依次展開講述:
public class Singleton{ private static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return singleton; }}
咱們來看看餓漢式的寫法,用static修飾咱們的實例,而且把構造函數用private修飾,這種寫法比較簡單,在類裝載的時候就完成了實例化,避免了線程同步的問題,缺點就在於類裝載的時候就完成了實例化,沒有達到懶加載的要求,若是從始至終都沒有使用過這個實例,就可能會形成內存的浪費。
還有一種方式與餓漢式比較相似,就是靜態代碼塊式;
public class Singleton{ private static Singleton singleton; static{ singleton = new Singleton(); } private Singleton(){} public static Singleton getInstance(){ return singleton; }}
這種方式和餓漢式相似,只不過把類實例化的過程放到了靜態代碼塊中,也是在類裝載的時候就執行了靜態代碼塊中的代碼,完成了實例化。因此這種方式和餓漢式的優缺點也是同樣的。
在瞭解了餓漢式的缺點以後咱們來看看第二種寫法,懶漢式,這種寫法在getInstance方法被調用的時候,纔去實例化咱們的實例,可是隻能在單線程下使用。若是在多線程下使用,若是一個線程進入了if(singleton == null)
判斷語句塊,還沒來得及往下執行,另外一個線程也經過了這個判斷語句,這時就會屢次建立實例。因此這裏須要注意,多線程環境下不能使用這種方式。
public class Singleton{ private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if (singleton == null) { singleton = new Singleton(); } return singleton; }}
若是要保證線程安全,咱們能夠對前邊的寫法進行升級,線程安全的懶漢式是怎麼樣的呢,咱們能夠在getInstance方法上加synchronized關鍵字,這樣就能夠解決上邊出現的線程安全問題。不過就是效率過低了,每一個線程在得到類的實例的時候,執行getInstance方法,都要進行同步,多個線程不能同時訪問,然而這在大多數狀況下都是沒有必要的。
public class Singleton{ private static Singleton singleton; private Singleton(){} public static synchronized Singleton getInstance(){ if (singleton == null) { singleton = new Singleton(); } return singleton; }}
這個地方有人會說,把synchronized關鍵字加在方法上效率過低了,那麼縮小範圍,把synchronized從方法上移除,而後把synchronized關鍵字放到了咱們的方法內部,採用了代碼塊的形式來保證線程安全,不過這種方法是有問題的,有可能產生多個實例。加入一個線程進入了第一個if(singleton == null)
判斷語句塊,還沒來得及往下執行,此時又一個線程經過了這個判斷,此時就會產生多個實例。
public static Singleton getInstance(){ if (singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); } } return singleton;}
雙重檢查模式的出現就是爲了解決上邊出現的問題,就有了雙重檢查模式。
public class Singleton{ private static volatile Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }}
咱們重點來看一下getInstance方法,咱們進行了兩次singleton == null
的判斷,就能夠保證線程安全了,這樣實例化代碼只用調用一次,後面再次訪問的時候,只會判斷第一次的if (singleton == null)
就能夠了,而後會跳過整個if塊,直接返回實例化對象,這種寫法的好處就是,不只線程安全,並且延遲加載,效率也更高。
這裏就會出現一個面試題,爲何是兩次判斷,去掉第二次的if判斷行不行?
這個時候須要考慮這樣一種狀況,有兩個線程同時調用了getInstance方法,而且因爲singleton是空的,因此兩個線程均可以經過第一個if判斷,而後因爲鎖機制,會有一個線程先進入同步語句,並進入第二個if判斷,而另一個線程須要等待鎖釋放,不過當第一個線程執行完new Singleton()
語句後,就會退出synchrinized保護的區域,這時若是沒有第二個判斷,那麼第二個線程也會建立一個實例,這就破壞了單例。那麼去掉第一個判斷,全部的線程都會串行執行,效率低下。綜上,兩個判斷都是須要保留的。
還有一個點須要注意,咱們給Singleton對象加了volatile關鍵字修飾,這又是爲何呢?
這主要在於singleton = new Singleton()
這句,這並非一個原子操做,在JVM中,這條語句至少作了三件事,第一步,給singleton分配內存空間;第二步,調用Singleton的構造函數等來初始化singleton;第三步,講singleton對象指向分配的內存空間(執行完這一步singleton就不是null了)。
這個地方須要注意一下1-2-3的順序,存在着重排序的優化,也就是說第二步和第三步的順序不能保證的,最終的執行順序多是1-2-3,也多是1-3-2。
若是順序是1-3-2,那麼第3步執行完以後,singleton就不是null了,但是此時並無執行第2步,假設此時又有一個線程進入了getInstance方法,因爲此時的singleton已經不是null了,就會經過第一個判斷,直接返回對象,其實這個時候的singleton並無完成初始化,因此使用這個實例的時候就會報錯。
使用volatile的意義就在於,它能夠防止上邊出現的那種重排序的發生,也就避免了拿到未完成初始化的對象。
靜態內部類的方式和餓漢式採用的機制有點相似,都採用了類裝載的機制,來保證咱們初始化實例時只有一個線程。因此在這個地方是由JVM實現的一個線程安全。
public class Singleton{ private Singleton(){} private static class SingletonInstance { private static final Singleton singleton = new Singleton(); } public static Singleton getInstance(){ return SingletonInstance.singleton; }}
餓漢式 的方式,在類被加載的時候,就會實例化對象,而靜態內部類方法在Singleton類被裝載時,並不會被馬上實例化,只有在調用getInstance方法的時候,纔會進行實例化。
看到這裏咱們已經學會了雙重檢查和靜態內部類兩種方法來線程 安全、高效、延遲加載的建立單例,這兩種方式都是不錯的寫法,可是它們不能防止被反序列化,生成多個實例。
藉助枚舉類來實現單例,這不只能避免多線程同步的問題,並且還能反正反序列化,和反射建立新的對象,來破壞單例狀況的出現。
public enum Singleton { INSTANCE; public void whatverMethod() { }}
至此,咱們已經學了五種方法實現單例,可是怎麼選擇呢,其實仍是優先推薦枚舉法,仍是要看看枚舉寫法的優勢,枚舉寫法的優勢:
其一是寫法簡單,不須要咱們去考慮線程安全和懶加載,代碼也比較短小精悍,是最簡練的寫法。
其二是線程安全有保障,經過反編譯一個枚舉類,咱們發現枚舉類中的各個枚舉項是經過static代碼塊來定義和初始化的,它們會在類加載的時候完成初始化,而Java類的加載由JVM保證線程安全,因此建立一個Enum類型枚舉類是線程安全的。前面幾種實現單例的方式都存在一些問題,那就是可能被反序列化破壞,反序列化生成的新的對象從而產生了多個實例。
其三是防止破壞單例,Java對於枚舉的序列化作了要求,僅僅是將枚舉類對象的name屬性輸出到結果中,在反序列化時,就是經過java.lang.Enum的valueOf方法,來根據名字查找對象,而不是新建一個新的對象,因此這就防止了反序列化致使的單例破壞問題的出現。對於反射破壞單例的問題,枚舉一樣有措施,反射在經過newInstance建立對象時,會檢查這個類是否是枚舉類,若是是枚舉類就拋出illegalArgumentException("Cannot reflectively create enum objects")
異常,反射建立對象失敗。能夠看出枚舉是能夠防止反序列化和發射破壞單例。這就是枚舉在實現單例上的優點。