【Android 系統開發】_「設計模式」篇 -- 單例

概述

什麼是單例模式?

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

這種模式涉及到一個單一的類,該類負責建立本身的對象,同時確保只有單個對象被建立。這個類提供了一種訪問其惟一的對象的方式,能夠直接訪問,不須要實例化該類的對象。面試

【注意】
      💥  單例類只能有一個實例。
      💥  單例類必須本身建立本身的惟一實例。
      💥  單例類必須給全部其餘對象提供這一實例。數據庫

咱們先來看看關於「單例模式」的如下幾點:設計模式

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

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

什麼時候使用:當您想控制實例數目,節省系統資源的時候。多線程

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

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

單例優勢: 一、在內存裏只有一個實例,減小了內存的開銷,尤爲是頻繁的建立和銷燬實例;二、避免對資源的多重佔用(好比寫文件操做)。優化

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

使用場景: 一、要求生產惟一序列號;二、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來;三、建立的一個對象須要消耗的資源過多,好比 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,並且還能防止反序列化致使從新建立新的對象。

相關文章
相關標籤/搜索