深入理解幾種單例模式的實現方式

  • 2020 年 4 月 10 日
  • 筆記

餓漢式

餓漢式的單例實現方式就是說在類載入的時候就已經創建並初始化好了,所以實例的創建過程是執行緒安全的

public class IdGenerator {      private AtomicLong id = new AtomicLong(0);      private static final IdGenerator instance = new IdGenerator();      private IdGenerator(){}      public static IdGenerator getInstance(){          return instance;      }      public long getId(){          return id.incrementAndGet();      }  }  

但是餓漢式是有一個缺點的,它不支援延時載入,也就是說實例在使用之前就已經創建好了,如果佔用資源過多,但又不使用,會造成一定的浪費,還是需要根據具體情況確定要不要使用這種方式。不過也有人說,如果耗時長,那在使用的時候再載入,就會影響性能,也是難為了餓漢了。

AtomicLong是一個原子變數類型,提供了執行緒安全的原子操作,這樣可以保證在多執行緒環境下,獲取id的時候不會出現重複ID的情況。

程式碼中的構造函數通過private修飾符進行修飾,保證了外部程式碼不能通過構造函數初始化IdGenerator類。

上面是一個簡單的唯一遞增ID號碼生成器,採用的餓漢式的單例實現模式,instance實例定義成了一個靜態常量,我們這知道在運行一個類的時候,先要載入到JVM中,類載入的過程分為三個階段,分別是:載入、鏈接和初始化,其中鏈接階段又分為三個步驟分別是:驗證、準備和解析,其中在準備的步驟中就會創建類或介面的靜態變數,並初始化靜態變數的初始值。

懶漢式

上面我們說到餓漢式是不支援延時載入到的,那懶漢式就支援延時載入了,懶漢式的實現方式是給獲取實例的方法加了鎖

public class IdGenerator {      private AtomicLong id = new AtomicLong(0);      private static IdGenerator instance;      private IdGenerator(){}        public static synchronized IdGenerator getInstance(){          if (instance == null){              instance = new IdGenerator();          }          return instance;      }      public long getId(){          return id.incrementAndGet();      }  }  

加鎖的結果就是性能降低,如果這個單例被頻繁的使用的話,那性能問題就會比較嚴重,需要考慮換種方式實現了。

雙重檢測

雙重檢測的單例實現方式彌補了上面餓漢式和懶漢式的缺點:不能延時載入和性能低的問題,具體實現方式就是在獲取的實例的時候先判斷是否已經創建過了,如果是就直接返回,這是第一重檢測,沒有的話,就進入同步塊,進入同步塊後再進行判斷實例是否已經存在,如果存在就直接返回,這是第二重檢測,如果不存在的話就在同步的情況下創建一個實例。

public class IdGenerator {      private AtomicLong id = new AtomicLong(0);      private static IdGenerator instance;      private IdGenerator(){          // 初始化程式碼      }      public static IdGenerator getInstance(){          if (instance == null){              synchronized (IdGenerator.class){ // 這裡指明synchronized保護的是當前的類對象                  if (instance == null){                      instance = new IdGenerator();                  }              }          }          return instance;      }      public long getId(){          return id.incrementAndGet();      }  }  

懶漢式的實現方式每次獲取實例的時候都要進同步程式碼,這樣就會造成多次的獲取鎖釋放鎖,造成性能損耗,但是雙重檢測實際上只需要同步一次創建實例就可以了,再獲取實例的時候是不用進同步塊程式碼的,這樣就大大提高了性能。

靜態內部類

靜態內部類的實現方式是一種比雙重檢測更加簡單的一種實現方式,而且既保證了性能又做到了延時載入

public class IdGenerator {      private AtomicLong id = new AtomicLong(0);      private IdGenerator(){          // 初始化程式碼      }      private static class SingletonHolder{          private static final IdGenerator instance = new IdGenerator();      }        public static IdGenerator getInstance(){          return SingletonHolder.instance;      }      public long getId(){          return id.incrementAndGet();      }  }  

SingletonHolder是一個靜態內部類,使用privat可以使內部類完全對外隱藏,當外部類IdGenerator載入的時候,並不會創建實例,只有當調用getInstance方法的時候才會載入SingletonHolder,並創建實例,這樣就具備了餓漢式的安全特性,同時也具備了延遲載入的特性。

枚舉

枚舉的實現方式是利用了java枚舉類型本身的特性,保證了實例創建的執行緒安全和實例的唯一性

public enum IdGenerator {      INSTANCE;      private AtomicLong id = new AtomicLong(0);      public long getId(){          return id.incrementAndGet();      }      public static void main(String[] args) {          IdGeneratorEnum.INSTANCE.getId();      }  }  

可以看出枚舉的實現方式是最簡潔的,由jvm保證執行緒安全和單一實例。還可以有效防止序列化和反序列化造成多個實例和利用反射創建多個實例的情況。枚舉類型在編譯成class文件後,再反編譯,

public final class IdGenerator extends java.lang.Enum<IdGenerator> {    public static final IdGenerator INSTANCE;    public static IdGenerator[] values();    public static IdGenerator valueOf(java.lang.String);    public long getId();    public static void main(java.lang.String[]);    static {};  }  

反編譯後的枚舉類其實繼承了Enum這個類,INSTANCE是一個靜態常量了,似乎又回到了餓漢式的那種那些,但是比它多出了延遲載入而且更加簡潔的特性

歡迎大家去 我的部落格 瞅瞅,裡面有更多關於測試實戰的內容哦!!

Tags: