iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 5
1
自我挑戰組

初探設計模式系列 第 5

[ Day 5 ] 初探設計模式 - 單例模式 (Singleton)

前言

今天是星期五,晚上公司有個聚餐(資深新人的歡迎會),

所以今天要生文章出來時間蠻吃緊的,

只好晚上熬夜稍微寫一點(其實是睡不著拖太晚),

另外星期五也是總結一週成果的時間點,

這星期都在開會、研討會、課程、寫鐵人賽文章

多少還是要顧一下工作上的進度和成果啊,

鐵人賽的文章比想像中還要花更多時間呢。

今天要介紹的是單例模式

這也是一個被大量廣泛應用的模式,而且實行起來很簡單,程式碼很短,但是有很多小細節需要注意(這些細節會因為語言而不同),不然可能會有難以發現的bug藏在裡面。

單例模式

定義:只有一個實例,而且自行實例化並向整個系統提供這個實例。

屬於創建模式,

這個模式涉及到一個單一的類別,他必須要創建自己的實例

並且確保只有單一個對象被創建

這個類別提供一個方法訪問其被創建的唯一一個對象。

存取IO和資料庫等資源,這時候要考慮使用單例模式。

UML

 Singleton

  • Singleton:很簡單的只有一個類別,其中提供存取自己物件的方法,確保整個系統只有實例化一個物件。
有幾種方式可以實現單例模式
  1. 懶散(Lazy)模式(線程不安全)
  2. 懶散模式(線程安全)
  3. 積極模式
  4. 雙重鎖 (Double ChockLock)
  5. 登記式(靜態內部類)
  6. 枚舉 (enumeration)

試著實現一個積極單例模式

public class SingleObject {
 
   //創建 SingleObject 的一個對象
   private static SingleObject instance = new SingleObject();
 
   //讓構造函數為 private,這樣該類就不會被實例化
   private SingleObject(){}
 
   //獲取唯一可用的對象
   public static SingleObject getInstance(){
      return instance;
   }
 
}

試著實現一個懶散單例模式

積極模式在宣告靜態物件的時候就已經初始化,

但是懶散模式(Lazy)在呼叫getInstance時才進行初始化。

public class Singleton{
    private static Singleton instance;
    //私有的建構式讓別人不能創造
    private Singleton (){}
    
    //因為整個系統都要存取這個類別,很可能有多個process或thread同時存取
    //為了讓線程安全添加synchronized在多線程下確保物件唯一性
    public static synchronized Singleton getInstance(){
        if (instance == null)
        {
            instance = new Singleton();
        }
        return instance;
    }
}

但是這個實現方式每次都需要進行同步,效率會很很低。

試著用雙重鎖實現

public class Singleton {
    public static Singleton instance;

    private Singleton(){}

    public static Singleton getInstance(){

//        第一層判斷為了避免不必要的同步
        if(instance == null){
            
            synchronized (Singleton.class){
//                第二層判斷為了在null的狀況下建立實例
                if(instance == null){
                    instance = new Singleton();
                }
            }

        }

        return instance;
    }
}

判斷兩次看起來有點奇怪,但其實這樣做是有原因的。

instance = new Singleton();

上面這段程式碼看起來只有一段,但其實他不是原子操作,這句程式碼會被編譯成多條組合指令,大致上他做了三件事:

  1. 給Singleton的實例分配記憶體;
  2. 呼叫Singleton的建構函數,初始化成員欄位;
  3. 將instance物件指向分配的記憶體空間(此時instance不是null)。

但是由於Java編譯器允許失序執行,所以 2. 和 3. 的順序是無法保證的,有可能 1-2-3 也有可能 1-3-2 。如果在 3. 執行完畢、2. 還沒執行之前,切換到線程B,那instance已經不是null,此時B取走instance再使用就會出錯。

JDK1.5以後的版本,官方注意到問題,所以調整JMM具體化volatile關鍵字,所以只要把instance寫法改成private volatile static Singleton instance = null;就可以保證都從主記憶體讀取,並且以DCL寫法完成單例模式。

雖然偶爾會失效但是DCL還是運用最多的模式。

試著用靜態內部類實現

public class StaticInnerClass {
    private StaticInnerClass(){}

    public static StaticInnerClass getInstance(){
        return StaticInnerClassHolder.instance;
    }

    /**
     * 靜態的內部類別
     */
    private static class StaticInnerClassHolder{
        private static StaticInnerClass instance = new StaticInnerClass();
    }
}

可以確保線程安全,保證物件唯一性,並且延遲實例化,所以推薦使用。

用列舉實現

public enum  EnumSingleton {
    INSTANCE;

    public void doSomething(){
        System.out.println("do do !");
    }
}

可以避免反實例化。

前面的單例模式要避免反實例化要加入readResolve()方法

private Object readResolve() throws ObjectStreamException {
    return instance;
}

這是提供給開發人員控制物件的反序列化方法。

試著寫出SingletonFactory

產品和工廠介面

public abstract class Product {
    public String getName(){
        return this.getClass().getSimpleName();
    }
}

public interface Factory {
    public Product getProduct();
}

可樂和漢堡(繼承了getName方法)

public class Cola extends Product {
}
public class Humberger extends Product {
}

SingletonFactory

public class SingletonFactory {

    public static Factory getColaFactory(){
        return ColaFactory.colaFactory;
    }

    public static Factory getHumbergerFactory(){
        return HumbergerFactory.humbergerFactory;
    }


    private static class ColaFactory implements Factory{

        private static ColaFactory colaFactory = new ColaFactory();

        private ColaFactory(){}

        @Override
        public Product getProduct() {
            return new Cola();
        }
    }

    private static class HumbergerFactory implements Factory{

        private static HumbergerFactory humbergerFactory = new HumbergerFactory();

        private HumbergerFactory(){}

        @Override
        public Product getProduct() {
            return new Humberger();
        }
    }
    
}

測試一下拿到可樂和漢堡產品

public class Test {

    public void test(){

        Cola cola = (Cola) SingletonFactory.getColaFactory().getProduct();
        Humberger humberger =(Humberger) SingletonFactory.getHumbergerFactory().getProduct();
        
        System.out.println(cola.getName());
        System.out.println(humberger.getName());

    }

}

拿到的數值

Cola
Humberger

今天關於單例模式就練習到這邊,如果有問題或建議歡迎留言或寄訊息給我~


上一篇
[ Day 4 ]初探設計模式 - 關於那些更基本的事情...系統架構(System Architecture)
下一篇
[ Day 6 ] 初探設計模式 - 深入工廠、策略與單例模式
系列文
初探設計模式30

尚未有邦友留言

立即登入留言