不少時候爲了節約系統資源,須要確保系統中某個類只有一個惟一的實例,當這個惟一實例建立了以後,沒法再建立一個同類型的其餘對象,全部的操做只能基於這一個惟一實例。這是單例模式的動機所在。java
好比Windows的任務管理器,能夠按Ctrl+Shift+Esc
啓動,並且啓動一個,不能啓動多個。編程
單例模式(Singleton Pattern):確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。安全
單例模式是一種對象建立型模式。微信
單例模式只有一個角色:多線程
Singleton
(單例角色):在單例類的內部只生成一個實例,同時它提供一個相似名叫getInstance
的靜態方法獲取實例,同時爲了防止外部生成新的實例化對象,構造方法可見性爲private
,在單例類內部定義了一個Singleton
的靜態對象,做爲供外部訪問的惟一實例new
等方式建立對象getInstance()
的公有靜態方法來獲取實例單例角色一般實現以下:併發
class Singleton { //餓漢式實現 private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance() { return instance; } }
客戶端直接經過該類獲取實例便可:負載均衡
Singleton singleton = Singleton.getInstance();
某個軟件須要使用一個全局惟一的負載均衡器,使用單例模式對其進行設計。
代碼以下:編程語言
public class LoadBalancer { private static LoadBalancer instance = null; private LoadBalancer(){} public static LoadBalancer getInstance() { return instance == null ? instance = new LoadBalancer() : instance; } public static void main(String[] args) { LoadBalancer balancer1 = LoadBalancer.getInstance(); LoadBalancer balancer2 = LoadBalancer.getInstance(); System.out.println(balancer1 == balancer2); } }
這是最簡單的單例類的設計,獲取實例時僅僅判斷是否爲null
,沒有考慮到線程問題。也就是說,多個線程同時獲取實例時,仍是有可能會產生多個實例,通常來講,常見的解決方式以下:函數
餓漢式單例就是在普通的單例類基礎上,在定義靜態變量時就直接實例化,所以在類加載的時候就已經建立了單例對象,並且在獲取實例時不須要進行判空操做直接返回實例便可:性能
public class LoadBalancer { private static LoadBalancer instance = new LoadBalancer(); private LoadBalancer(){} public static LoadBalancer getInstance() { return instance; } }
當類被加載時,靜態變量instance
被初始化,類的私有構造方法將被調用,單例類的惟一實例將被建立。
懶漢式單例在類加載時不進行初始化,在須要的時候再初始化,加載實例,同時爲了不多個線程同時調用getInstance()
,能夠加上synchronized
:
public class LoadBalancer { private static LoadBalancer instance = null; private LoadBalancer(){} synchronized public static LoadBalancer getInstance() { return instance == null ? instance = new LoadBalancer() : instance; } }
這種技術又叫延遲加載技術,儘管解決了多個線程同時訪問的問題,可是每次調用時都須要進行線程鎖定判斷,這樣會下降效率。
事實上,單例的核心在於instance = new LoadBalancer()
,所以只須要鎖定這行代碼,優化以下:
public static LoadBalancer getInstance() { if(instance == null) { synchronized (LoadBalancer.class) { instance = new LoadBalancer(); } } return instance; }
可是實際狀況中仍是有可能出現多個實例,由於若是A和B兩個線程同時調用getInstance()
,都經過了if(instance == null)
的判斷,假設線程A先得到鎖,建立實例後,A釋放鎖,接着B獲取鎖,再次建立了一個實例,這樣仍是致使產生多個單例對象。
所以,一般採用一種叫「雙重檢查鎖定」的方式來確保不會產生多個實例,一個線程獲取鎖後再進行一次判空操做:
private volatile static LoadBalancer instance = null; public static LoadBalancer getInstance() { if(instance == null) { synchronized (LoadBalancer.class) { if(instance == null) { instance = new LoadBalancer(); } } } return instance; }
須要注意的是要使用volatile
修飾變量,volatile
能夠保證可見性以及有序性。
爲了克服餓漢式不能延遲加載以及懶漢式的線程安全控制繁瑣問題,可使用一種叫Initialization on Demand Holder(IoDH)
的技術。實現IoDH時,需在單例類增長一個靜態內部類,在該內部類中建立單例對象,再將該單例對象經過getInstance()
方法返回給外部使用,代碼以下:
public class LoadBalancer { private LoadBalancer(){} private static class HolderClass { private static final LoadBalancer instance = new LoadBalancer(); } public static LoadBalancer getInstance() { return HolderClass.instance; } }
因爲單例對象沒有做爲LoadBalancer
的成員變量直接實例化,所以類加載時不會實例化instance
。首次調用getInstance()
時,會初始化instance
,由JVM保證線程安全性,確保只能被初始化一次。另外相比起懶漢式單例,getInstance()
沒有線程鎖定,所以性能不會有任何影響。
經過IoDH既能夠實現延遲加載,又能夠保證線程安全,不影響系統性能,可是缺點是與編程語言自己的特性相關,不少面嚮對象語言不支持IoDH。另外,還可能引起NoClassDefFoundError
(當初始化失敗時),例子能夠戳這裏。
其中,不管是餓漢式,仍是懶漢式,仍是IoDH,都有或多或少的問題,而且還能夠經過反射以及序列化/反序列化方式去「強制」生成多個單例,有沒有更優雅的解決方案呢?
有!答案就是枚舉。
代碼以下:
public class Test { public static void main(String[] args) { LoadBalancer balancer1 = LoadBalancer.INSTANCE; LoadBalancer balancer2 = LoadBalancer.INSTANCE; System.out.println(balancer1 == balancer2); } } enum LoadBalancer{ INSTANCE; }
使用枚舉實現單例優勢以下:
若是以爲文章好看,歡迎點贊。
同時歡迎關注微信公衆號:氷泠之路。