單例模式的文章能夠說是百家爭鳴,今天我也來講道說道,你們共同提高。html
確保某一個類只有一個實例,並且能夠自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。 單例模式是一種對象建立型模式。數據庫
好比一個應用中應該只存在一個ImageLoader實例。設計模式
Android中的LayoutInflater類等。安全
EventBus中getDefault()方法獲取實例。bash
這三步怎麼用代碼體現呢?微信
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
複製代碼
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
複製代碼
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
複製代碼
上面三種寫法本質上實際上是同樣的,也是各種文章在介紹餓漢式時經常使用的方式。但使用靜態final的實例對象或者使用靜態代碼塊依舊不能解決在反序列化、反射、克隆時從新生成實例對象的問題。多線程
序列化:一是能夠將一個單例的實例對象寫到磁盤,實現數據的持久化;二是實現對象數據的遠程傳輸。 當單例對象有必要實現 Serializable 接口時,即便將其構造函數設爲私有,在它反序列化時依然會經過特殊的途徑再建立類的一個新的實例,至關於調用了該類的構造函數有效地得到了一個新實例!併發
反射:能夠經過setAccessible(true)來繞過 private 限制,從而調用到類的私有構造函數建立對象。框架
克隆:clone()是 Object 的方法,每個對象都是 Object 的子類,都有clone()方法。clone()方法並非調用構造函數來建立對象,而是直接拷貝內存區域。所以當咱們的單例對象實現了 Cloneable 接口時,儘管其構造函數是私有的,仍能夠經過克隆來建立一個新對象,單例模式也相應失效了。函數
優勢:寫法比較簡單,在類裝載的時候就完成實例化。避免了線程同步問題。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。若是從始至終從未使用過這個實例,則會形成內存的浪費。
那麼咱們就要考慮懶加載的問題了。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance== null) {
instance = new Singleton();
}
return instance;
}
}
複製代碼
優勢:懶加載,只有使用的時候纔會加載。
缺點:可是隻能在單線程下使用。若是在多線程下,instacnce對象仍是空,這時候兩個線程同時訪問getInstance()方法,由於對象仍是空,因此兩個線程同時經過了判斷,開始執行new的操做。因此在多線程環境下不可以使用這種方式。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
複製代碼
優勢:懶加載,只有使用的時候纔會加載,獲取單例方法加了同步鎖,保正了線程安全。
缺點:效率過低了,每一個線程在想得到類的實例時候,執行getInstance()方法都要進行同步。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
複製代碼
優勢:改進了第五種效率低的問題。
缺點:但實際上這個寫法還不能保證線程安全,和第四種寫法相似,只要兩個線程同時進入了 if (singleton == null) { 這句判斷,照樣會進行兩次new操做
接下來就是聽起來很牛逼的雙重檢測加鎖的單例模式。
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;
}
}
複製代碼
本例的亮點都在getInstance()方法上,能夠看到在該方法中對instance進行了兩次判空:第一層判斷爲了不沒必要要的同步,第二層判斷則是爲了在null的狀況下建立實例。對第六種單例的漏洞進行了彌補,可是仍是有丶小問題的,問題就在instance = new Singleton();語句上。
這語句在這裏看起來是一句代碼啊,但實際上它並非一個原子操做,這句代碼最終會被編譯成多條彙編指令,它大體作了3件事情:
可是,因爲Java編譯器運行處理器亂序執行,以及jdk1.5以前Java內存模型中Cache、寄存器到主內存會寫順序的規定,上面的第二和第三的順序是沒法保證的。也就是說,執行順序多是1-2-3也多是1-3-2.若是是後者,而且在3執行完畢、2未執行以前,被切換到線程B上,這時候instance由於已經在線程A內執行3了,instance已是非null,全部線程B直接取走instance,再使用時就會出錯,這就是DCL失效問題,並且這種難以跟蹤難以重現的問題極可能會隱藏好久。
優勢:線程安全;延遲加載;效率較高。
缺點:JVM編譯器的指令重排致使單例出現漏洞。
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;
}
}
複製代碼
在jdk1.5以後,官方已經注意到這種問題,調整了JVM、具體化了volatile關鍵字,所以,若是是1.5或以後的版本,只須要將instance的定義改爲private static volatile Singleton instance = null;
就能夠保證instance對象每次都是從主內存中讀取,就可使用DCL的寫法來完成單例模式。固然,volatile多少會影響到性能,但考慮到程序的正確性,犧牲這點性能仍是值得的。
優勢:線程安全;延遲加載;效率較高。
缺點:因爲volatile關鍵字會屏蔽Java虛擬機所作的一些代碼優化,略微的性能下降,但除非你的代碼在併發場景比較複雜或者低於JDK6版本下使用,不然,這種方式通常是可以知足需求的。
public class Singleton {
private Singleton() {
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
複製代碼
這種方式跟餓漢式方式採用的機制相似,但又有不一樣。 二者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不一樣的地方在餓漢式方式是隻要Singleton類被裝載就會實例化,沒有Lazy-Loading的做用,而靜態內部類方式在Singleton類被裝載時並不會當即實例化,而是在須要實例化時,調用getInstance方法,纔會裝載SingletonInstance類,從而完成Singleton的實例化。
因此在這裏,利用 JVM的 classloder 的機制來保證初始化 instance 時只有一個線程。JVM 在類初始化階段會獲取一個鎖,這個鎖能夠同步多個線程對同一個類的初始化
優勢:避免了線程不安全,延遲加載,效率高。
缺點:依舊不能解決在反序列化、反射、克隆時從新生成實例對象的問題。
public enum Singleton {
INSTANCE
}
複製代碼
枚舉類單例模式是《Effective Java》做者 Josh Bloch 極力推薦的單例方法
藉助JDK 1.5中添加的枚舉來實現單例模式。P.S. Enum是沒有clone()方法的
優勢:寫法簡單,不只能避免多線程同步問題,並且還能防止反序列化、反射,克隆從新建立新的對象。
缺點:JDK 1.5以後才能使用。
public class SingletonManger {
private static Map<String, Object> objectMap = new HashMap<String, Object>();
private SingletonManger() {
}
public static void registerService(String key, Object instance) {
if (!objectMap.containsKey(key)) {
objectMap.put(key, instance);
}
}
public static Object getService(String key) {
return objectMap.get(key);
}
}
複製代碼
查閱Android源碼中的 LayoutInflater 對象就能發現使用了這種寫法
優勢:在程序的初始,將多種單例類型注入到一個統一的管理類中,在使用時根據key獲取對象對應類型的對象。這種方式使得咱們能夠管理多種類型的單例,而且在使用時能夠經過統一的接口進行獲取操做, 下降了用戶的使用成本,也對用戶隱藏了具體實現,下降了耦合度。
缺點:不經常使用,有些麻煩
在微信公衆號看到有大佬說使用枚舉配合內部類實現內部枚舉類,能夠達成線程安全,懶加載,責任單一原則,等等是如今最完美的寫法。
若是你和我同樣是Android開發,那麼因爲在客戶端一般沒有高併發的狀況,選擇哪一種實現方式並不會有太大的影響。但即使如此,出於效率考慮咱們也應該使用後面幾種單例方法。
單例模式的優勢其實已經在定義中提現了:能夠減小系統內存開支,減小系統性能開銷,避免對資源的多重佔用、同時操做。
在Android 應用啓動後、任意組件被建立前,系統會自動爲應用建立一個 Application類(或其子類)的對象,且只建立一個。今後它就一直在那裏,直到應用的進程被殺掉。
因此雖然 Application並無採用單例模式來實現,可是因爲它的生命週期由框架來控制,和整個應用的保持一致,且確保了只有一個,因此能夠被看做是一個單例。 可是若是你直接用它來存取數據,那你將獲得無窮無盡的NullPointerException。
由於Application 不會永遠駐留在內存裏,隨着進程被殺掉,Application 也被銷燬了,再次使用時,它會被從新建立,它以前保存下來的全部狀態都會被重置。
要預防這個問題,咱們不能用 Application 對象來傳遞數據,而是要:
經過傳統的 intent 來顯式傳遞數據(將 Parcelable 或 Serializable 對象放入Intent / Bundle。Parcelable 性能比 Serializable 快一個量級,可是代碼實現要複雜一些)。
重寫onSaveInstanceState()以及onRestoreInstanceState()方法,確保進程被殺掉時保存了必須的應用狀態,從而在從新打開時能夠正確恢復現場。
使用合適的方式將數據保存到數據庫或硬盤。
老是作判空保護和處理。
參考文章
《Android 源碼設計模式解析與實戰》