Java設計模式之單例設計模式

Java設計模式之單例設計模式

1. 單例設計模式實現有八種方式:

  1. 餓漢式(靜態常量)
  2. 餓漢式(靜態代碼塊)
  3. 懶漢式(線程不安全)
  4. 懶漢式(線程安全,同步方法)
  5. 懶漢式(線程安全,同步代碼塊)
  6. 雙重檢查
  7. 靜態內部類
  8. 枚舉

2. 餓漢式(靜態常量)實現單例

2.1. 實現步驟:

(1) 構造器私有化

(2) 類的內部創建對象

(3) 向外暴露一個靜態的公共方法。

2.2 代碼實現

class Singleton {
    // 1. 構造器私有化, 外部能new
    private Singleton () {}
    // 2. 內部可以創建對象實例
    private final static Singleton  instance = new Singleton();
    // 3. 提供一個靜態方法,返回實例對象
    public static Singleton getInstance() {
        return instance;
    }
}

2.3. 優缺點說明:

  1. 優點: 這種寫法比較簡單,就在類裝載的時候完成實例化,避免了線程同步問題。
  2. 缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始到終未曾使用這個類,則會導致內存浪費。
  3. 這種方式基於classloader機制避免了多線程同步問題,不過,instance在類的裝載時就實例化,在單例模式中大多數都是調用getInstance方法,但是導致類裝載的原因很多,因此不確定有其他方式導致類裝載,這時候初始化instance就沒有達到lazy loading的效果。
  4. 結論:這種單利模式可用,可能造成內存浪費。

3. 餓漢式(靜態代碼塊)實現單例

3.1 代碼示例

class Singleton {
    // 1. 構造器私有化, 外部能new
    private Singleton () {}
    // 2. 內部可以創建對象實例
    private static Singleton instance;
    // 在靜態代碼塊中,創建單利對象
    static {
        instance = new Singleton();
    }
    // 3. 提供一個靜態方法,返回實例對象
    public static Singleton getInstance() {
        return instance;
    }
}

3.2 優缺點說明

  1. 這種方式和上面的實現方式其實類似,只不過將類實例化的過程放在靜態代碼快總,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。優缺點和餓漢式的靜態常量的一樣。
  2. 結論:這種單利模式可用,可能造成內存浪費。

4. 懶漢式(線程不安全)實現單例

4.1 代碼示例:

public class Singleton {
    private static Singleton instance;
    
    private Singleton () {}
    // 提供一個靜態共有的方法,當時用該方法時,纔去創建instance
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

4.2 優缺點說明:

  1. 起到了Lazy Loading的效果,但是只能在單線程下使用。
  2. 如果在多線程下,一個線程進入if (singleton == null)語句塊,還未來的及往下執行,另一個線程也通過了這條判斷語句,這是遍會產生多個實例,所以在多線程環境下不可使用這種方式。
  3. 結論: 再實際開發中,不可以使用這種方式。

5. 懶漢式(線程安全,同步方法)實現單例

5.1 代碼示例

public class Singleton {
    private static Singleton singleton;
    
    private Singleton() {}
    
    // 加入同步處理代碼
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

5.2 優缺點說明:

  1. 解決了線程不安全問題
  2. 效率太低,每一個線程在想獲得類的實例的時候,執行getInstance方法都要執行通過,而其實這個方法只執行一次實例化代碼就可以,後面想獲得該實例直接return就可以了,方法進行同步的效率太低了。
  3. 結論: 在實際開發中,不推薦使用這種方式。

6 懶漢式(線程安全,同步代碼塊)實現單例

6.1 代碼示例

public class Singleton {
    private static Singleton singleton;
    
    private Singleton() {}
    
    // 加入同步處理代碼
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

6.2 優缺點說明:

  1. 這種方式,本意是想對第四種實現方式的改進,因爲前面同步方法效率太低,

改爲同步產生實例化的的代碼塊

  1. 但是這種同步並不能起到線程同步的作用。跟第3種實現方式遇到的情形一

致,假如一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,

另一個線程也通過了這個判斷語句,這時便會產生多個實例

  1. 結論:在實際開發中,不能使用這種方式

7. 雙重檢查實現單例

7.1 示例代碼

public class Singleton {
    // volatile 禁止重排序,
    private static volatile Singleton singleton;
    
    private Singleton() {}
    // 重排問題
    public static Singleton getInstance () {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton == new Singleton();
                }
            }
        }
        return singleton;
    }
}

7.2 優缺點說明:

  1. Double-Check概念是多線程開發中常使用到的,如代碼中所示,我們進行了兩

if (singleton == null)檢查,這樣就可以保證線程安全了。

  1. 這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if (singleton == null)

直接return實例化對象,也避免的反覆進行方法同步。

  1. 線程安全;延遲加載;效率較高 。

  2. 結論:在實際開發中,推薦使用這種單例設計模式。

7.3 指令重排對單例造成的影響

7.3.1 什麼是指令重排

比如 java 中簡單的一句 instance = new Singleton,會被編譯器編譯成如下 JVM 指令:

memory =allocate();    //1:分配對象的內存空間 
ctorInstance(memory);  //2:初始化對象 
instance =memory;     //3:設置instance指向剛分配的內存地址

但是這些指令順序並非一成不變,有可能會經過 JVM 和 CPU 的優化,指令重排成下面的順序:

memory =allocate();    //1:分配對象的內存空間 
instance =memory;     //3:設置instance指向剛分配的內存地址 
ctorInstance(memory);  //2:初始化對象
7.3.2 影響

對應到上文的單例模式,會產生:

  1. 當線程 A 執行完1,3,時,準備走2,即 instance 對象還未完成初始化,但已經不再指向 null 。
  2. 此時如果線程 B 搶佔到CPU資源,執行 if(instance == null)的結果會是 false,
  3. 從而返回一個沒有初始化完成的instance對象
7.3.3 解決
// volatile 禁止重排序,
private static volatile Singleton singleton;

很簡單,volatile 修飾符在此處的作用就是阻止變量訪問前後的指令重排,從而保證了指令的執行順序。

意思就是,指令的執行順序是嚴格按照上文的 1、2、3 來執行的,從而對象不會出現中間態。

其實,volatile 關鍵字在多線程的開發中應用很廣,暫不贅述。

8. 靜態內部類實現單例

8.1 示例代碼

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

8.2 優缺點說明

  1. 這種方式採用了類裝載的機制來保證初始化實例時只有一個線程。

  2. 靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化

時,調用getInstance方法,纔會裝載SingletonInstance類,從而完成Singleton

實例化。

  1. 類的靜態屬性只會在第一次加載類的時候初始化,所以在這裏,JVM幫助我們

保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。

  1. 優點:避免了線程不安全,利用靜態內部類特點實現延遲加載,效率高 。

  2. 結論:推薦使用。

9. 以上實現會遇到的問題

上面的實現方法都包含了一個 private 的構造函數。因此,這是不是意味着,我們就能保證無法創建多個類的實例了呢?

答案是否定的,即我們仍然有其他的高階方法來創建多個類的實施,以破解單例模式。

  • 序列化(serialization)/ 反序列化(deserializations)
  • 反射(reflection)

9.1 序列化(SERIALIZATION)/ 反序列化(DESERIALIZATIONS)問題

序列化可能會破壞單例模式。

通過比較一個序列化後的對象實例和其被反序列化後的對象實例,我們發現他們不是同一個對象,換句話說,在反序列化時會創建一個新的實例(即使定義構造函數爲 private)。

9.1.1 解決方案

在反序列化過程中,如果被序列化的類中定義了 readResolve 方法,虛擬機會試圖調用對象類裏的 readResolve 方法,以進行用戶自定義的反序列化。

最終,實現了在序列化 / 反序列化過程中也不破壞單例模式。

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     

   protected Singleton() {     
   }  

   //反序列時直接返回當前INSTANCE
   private Object readResolve() {     
   		return INSTANCE;     
   }    
}

9.2 反射(REFLECTION)

類似地,使用反射(reflection)可以強行調用私有構造器。

9.2.1 解決
public static Singleton INSTANCE = new Singleton();     
private static volatile  boolean  flag = true;
private Singleton(){
    if(flag){
    		flag = false;   
    }else{
        throw new RuntimeException("The instance  already exists !");
    }
}

10 枚舉實現單例

10.1 示例代碼

public enum SingletonEnum {
    INSTANCE;
    int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

10.2 優缺點說明

優缺點說明

  1. 這藉助JDK1.5中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而

且還能防止反序列化重新創建新的對象。

  1. 這種方式是Effective Java作者Josh Bloch提倡的方式
  2. 使用枚舉的方法是起到了單例的作用,但是也有一個弊端,那就是 無法進行懶加載
  3. 結論:推薦使用

11. 單例模式注意事項和細節說明

  1. 單例模式保證了 系統內存中該類只存在一個對象,節要頻繁創建銷燬的對象,使用單例模式可以提高系統
  2. 當想實例化一個單例類的時候,必須要記住使用相應用new
  3. 單例模式使用的場景:需要頻繁的進行創建和銷燬的耗費資源過多(即:重量級對象),但又經常用到的對據庫或文件的對象(比如數據源、session工廠等)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章