我們的态度是:每天進步一點點,理想終會被實作。
前言
繼上一篇Android常用設計模式之工廠模式,今天給大家講解一篇Android常用的設計模式——單例模式。我想單例模式應該是最常用的模式之一,可能很多老鐵認為單例模式已經是熟悉的不行了,但是我還是要寫一篇,作為記錄。
設計模式之單例模式
單例模式
說起單例模式,我想大家可能都清楚,通常我們的APP的一個類,在運作的時候可能有很多個對象,但是我們的單例模式不一樣,在我們的類中隻存在一個執行個體對象。
那麼在我們Android中經常在哪些場景會使用到我們的單例模式呢?
資料庫連接配接、線程池、配置檔案解析加載等一些非常耗時,占用系統資源的操作,并且還存在頻繁建立和銷毀對象,如果每次都建立一個執行個體,這個系統開銷是非常恐怖的,是以,我們可以始終使用一個公共的執行個體,以節約系統開銷。
- 靜态成員變量
- 私有構造方法
- 全局通路
單例模式的分類
- 餓漢模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
這種模式是隻要類一加載,那麼我們就建立了對象
我們測試一下:
public class SingletonTest {
@Test
public void getInstance(){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("對象1:" + s1.hashCode());
System.out.println("對象2:" + s2.hashCode());
if (s1 == s2) {
System.out.println("對象相等");
} else {
System.out.println("對象不等");
}
}
}
此時,我們調用兩次 Singleton 類的 getInstance() 方法來擷取 Singleton 的執行個體。我們發現 s1 和 s2 是同一個對象。
- 懶漢模式
懶漢模式,是一種延遲加載。在我們建立類的時候是不會立馬建立對象,隻有在我們需要的時候才會加載。
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance = null;
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
這種模式有個缺點就是線程并不安全,當兩個線程同時調用的時候,線程1調用了getInstance()的時候,instance并沒有建立完成,這時線程2又在調用getInstance(),又會從新建立一次對象,此時就不能保證我們的對象單一性了,是以線程并不安全。
- 懶漢模式線程安全版
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance = null;
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
上面的案例,在多線程中工作且線程安全,但是每次調用 getInstance() 方法都需要進行線程鎖定判斷,在多線程高并發通路環境中,将會導緻系統性能下降。事實上,不僅效率很低,99%情況下不需要線程鎖定判斷。
- 懶漢模式線程安全進階版
public class Singleton4 {
private Singleton4(){}
private static Singleton4 instance = null;
public static Singleton4 getInstance(){
if(instance == null){
synchronized(Singleton4.class){
if(instance == null){
instance = new Singleton4();
}
}
}
return instance;
}
}
這種性能方面就優于前面那種,這種模式等于兩次校驗,第一次是判斷是否建立對象,如果沒有再以同步的方式建立對象。
public class Singleton5 {
private Singleton5() {}
private static class SigletonHolder {
private final static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance() {
return SigletonHolder.instance;
}
}
- 枚舉
public enum SingletonEnum {
INSTANCE;
private SingletonEnum(){}
}
枚舉的特點是,構造方法是 private 修飾的,并且成員對象執行個體都是預定義的,是以我們通過枚舉來實作單例模式非常的便捷。這種模式很少見,而且我們也不常用,本身枚舉在性能方面差,占用記憶體較多,隻需要了解即可。
- 靜态内部内
public class Singleton5 {
private Singleton5() {}
private static class Sigleton {
private final static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance() {
return SigletonHolder.instance;
}
}
類加載的時候并不會執行個體化 Singleton5,而是在第一次調用 getInstance() 加載内部類 Sigleton,此時才進行初始化 instance 成員變量,確定記憶體中的對象唯一性。
單例模式 vs 靜态方法
如果認為單例模式是非靜态方法。而靜态方法和非靜态方法,最大的差別在于是否常駐記憶體,實際上是不對的。它們都是在第一次加載後就常駐記憶體,是以方法本身在記憶體裡,沒有什麼差別,是以也就不存在靜态方法常駐記憶體,非靜态方法隻有使用的時候才配置設定記憶體的結論。
是以,我們要從場景的層面來剖析這個問題。如果一個方法和他所在類的執行個體對象無關,僅僅提供全局通路的方法,這種情況考慮使用靜态類,例如 java.lang.Math。而使用單例模式更加符合面向對象思想,可以通過繼承和多态擴充基類。此外,上面的案子中,單例模式還可以進行延伸,對執行個體的建立有更自由的控制。
volatile 修飾
對象的建立并不是一個原子操作,在 new 對象的時候其實是有 3 步:配置設定記憶體,初始化和指派。由于 java 是允許處理器進行亂序執行的,是以有可能是先指派再初始化,這樣懶漢模式就有異常了,解決方法是給這個靜态對象加 volatile 字段來防止亂序執行。
這裡對volatile不做詳細的解釋,感興趣的可以檢視(https://www.cnblogs.com/dolphin0520/p/3920373.html)
總結
我們始終記得一個原則就是:單例模式始終保證一個類隻有一個執行個體對象存在,及唯一性。
如果采用餓漢式,在類被加載時就執行個體化,是以無須考慮多線程安全問題,并且對象一開始就得以建立,性能方面要優于懶漢式。
如果采用懶漢式,采用延遲加載,在第一次調用 getInstance() 方法時才執行個體化。好處在于無須一直占用系統資源,在需要的時候再進行加載執行個體。但是,要特别注意多線程安全問題,我們需要考慮使用雙重校驗鎖的方案進行優化。
實際上,我們應該采用餓漢式還是采用懶漢式,取決于我們希望空間換取時間,還是時間換取空間的抉擇問題,是以選擇哪種模式,隻是取決于我們實際項目中适合哪種。
最後想說一點,靜态内部類也是非常不錯的實作方式。
溫馨提示:
我建立了一個技術交流群,群裡有各個行業的大佬都有,大家可以在群裡暢聊技術方面内容,以及文章推薦;如果有想加入的夥伴加我微信号【luotaosc】備注一下“加群” 另外關注公衆号,還有一些個人收藏的視訊:
回複“學習資源” ,擷取學習視訊。
原創文章不易,如果覺得寫得好,掃碼關注一下點個贊,是我最大的動力。
關注我,一定會有意想不到的東西等你: 每天專注分享Android幹貨

備注:程式圈LT