Android 8.1 源碼_設計篇 -- 單例模式

概述

什麼是單例模式?

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

【注意】
      💥  單例類只能有一個實例。
      💥  單例類必須自己創建自己的唯一實例。
      💥  單例類必須給所有其他對象提供這一實例。

我們先來看看關於“單例模式”的以下幾點:

單例意圖:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

主要解決:一個全局使用的類頻繁地創建與銷燬。

何時使用:當您想控制實例數目,節省系統資源的時候。

如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則創建。

關鍵代碼:構造函數是私有的。

單例優點: 1、在內存裏只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷燬實例;2、避免對資源的多重佔用(比如寫文件操作)。

單例缺點:沒有接口,不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化。

使用場景: 1、要求生產唯一序列號;2、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來;3、創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。

注意事項:getInstance()方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化。

單例 DEMO

接下來我們看個簡單的單例設計的 Demo,我們先創建一個 Singleton 類:SingleObject.java

public class SingleObject {
 
   //創建 SingleObject 的一個對象
   private static SingleObject instance = new SingleObject();
 
   //讓構造函數爲 private,這樣該類就不會被實例化
   private SingleObject(){}
 
   //獲取唯一可用的對象
   public static SingleObject getInstance(){
      return instance;
   }
 
   public void showMessage(){
      System.out.println("Hello World!");
   }
}

然後從 singleton 類獲取唯一的對象:SingletonPatternDemo.java

public class SingletonPatternDemo {
   public static void main(String[] args) {
 
      //不合法的構造函數
      //編譯時錯誤:構造函數 SingleObject() 是不可見的
      //SingleObject object = new SingleObject();
 
      //獲取唯一可用的對象
      SingleObject object = SingleObject.getInstance();
 
      //顯示消息
      object.showMessage();
   }
}

我們看下執行結果:

Hello World!

單例模式的實現方式

餓漢式

代碼

public class Singleton {  

    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    
    public static Singleton getInstance() {  
        return instance;  
    }  
}

說明

這種方式比較常見,典型的“餓漢式”寫法。

【是否多線程安全】:是
【實現難度】:易
【優點】:沒有加鎖,執行效率會提高。
【缺點】:類加載時就初始化,浪費內存。

改進版:懶漢式 - 線程不安全

代碼

public class Singleton {  

    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

說明

這種方式是大多數面試者的寫法,也是教科書上的標配,但這段代碼卻存在一個致命的問題:當多個線程並行調用 getInstance() 的時候,就會創建多個實例。

改進版:懶漢式 - 線程安全

代碼

public class Singleton {  

    private static Singleton instance;  
    private Singleton (){}  
    
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

說明

既然要線程安全,那就如上所述“加鎖”處理!

【是否多線程安全】:是
【實現難度】:易
【優點】:第一次調用才初始化,避免內存浪費。
【缺點】:必須加鎖 synchronized 才能保證單例,但加鎖(加鎖操作也是耗時的)會影響效率。

改進版:雙重校驗鎖

代碼

public class Singleton {  

    private static Singleton singleton;  
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

說明

爲什麼需要進行 2 次判斷是否爲空呢?

第一次判斷是爲了避免不必要的同步,第二次判斷是確保在此之前沒有其他進程進入到 synchronized 塊創建了新實例。

這段代碼看起來很完美,很可惜,它還是有隱患。主要在於 instance = new Singleton() 這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:

      ✨ 給 instance 分配內存
      ✨ 調用 Singleton 的構造函數來初始化成員變量
      ✨ 將 instance 對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。

改進版:雙檢鎖(volatile)

代碼

public class Singleton {  

    private volatile static Singleton singleton;  
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}

說明

有些人認爲使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操作後面會有一個內存屏障(生成的彙編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操作都先行發生於後面對這個變量的讀操作(這裏的“後面”是時間上的先後順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,所以在這之後纔可以放心使用 volatile。

那麼,有沒有一種既有懶加載,又保證了線程安全,還簡單的方法呢?

當然有,靜態內部類,就是一種我們想要的方法。我們完全可以把 Singleton 實例放在一個靜態內部類中,這樣就避免了靜態實例在 Singleton 類加載的時候就創建對象,並且由於靜態內部類只會被加載一次,所以這種寫法也是線程安全的。

終極版:靜態內部類

代碼

public class Singleton {  

    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}

說明

這是比較推薦的解法,這種寫法用 JVM 本身的機制保證了線程安全的問題,同時讀取實例的時候也不會進行同步,沒什麼性能缺陷,還不依賴 JDK 版本。

枚舉

代碼

public enum Singleton {  
    INSTANCE;  
}

說明

這是從 Java 1.5 發行版本後就可以實用的單例方法,我們可以通過 Singleton.INSTANCE 來訪問實例,這比調用 getInstance() 方法簡單多了。

創建枚舉默認就是線程安全的,所以不需要擔心 double checked locking,而且還能防止反序列化導致重新創建新的對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章