單例模式的幾種實現方式及對比

  • 2019 年 10 月 3 日
  • 筆記

所謂單例就是在系統中只有一個該類的實例。
單例模式的核心分以下三個步驟:

  1. 構造方法私有化。即不能在類外實例化,只能在類內實例化。
  2. 在本類中創建本類的實例。
  3. 在本類中提供給外部獲取實例的方式。

單例模式的實現方式有兩種:餓漢模式和懶漢模式。

餓漢模式

不管現在需不需要,先創建實例。關鍵在於“餓”,餓了就要立即吃。

靜態常量

這裡將類的構造器私有化,就不能在外部通過new關鍵字創建該類的實例,然後定義了一個該類的常量,用static修飾,以便外部能夠獲得該類實例(通過HungryStaticConstantSingleton.INSTANCE 獲得)。也可以不加final關鍵字,具體看自己的需求。或者將INSTANCE私有化,並提供靜態方法getInstance返回INSTANCE實例。

 1 /**   2  * 惡漢模式-靜態常量,簡潔直觀   3  */   4 public class HungryStaticConstantSingleton{   5     //構造器私有化   6     private HungryStaticConstantSingleton() {   7     }   8     //靜態變數保存實例變數 並提供給外部實例   9     public final static HungryStaticConstantSingleton INSTANCE = new HungryStaticConstantSingleton();  10 }

 

優點
  • 由於使用了static關鍵字,保證了在引用這個變數時,關於這個變數的所以寫入操作都完成,所以保證了JVM層面的執行緒安全。
缺點
  • 不能實現懶載入,造成空間浪費。如果一個類比較大,我們在初始化的時就載入了這個類,但是我們長時間沒有使用這個類,這就導致了記憶體空間的浪費。

枚舉

這種方式是最簡潔的,不需要考慮構造方法私有化。值得注意的是枚舉類不允許被繼承,因為枚舉類編譯後默認為final class,可防止被子類修改。常量類可被繼承修改、增加欄位等,容易導致父類的不兼容。枚舉類型是執行緒安全的,並且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現單例模式,枚舉的寫法非常簡單,而且枚舉類型是所用單例實現中唯一一種不會被破壞的單例實現模式

/**   * 惡漢-枚舉形式,最簡潔   */  public enum HungryEnumSingleton{      INSTANCE;        public void print(){          System.out.println("這是通過枚舉獲得的實例");          System.out.println("HungryEnumSingleton.pring()");      }  }

 

Test,列印實例直接輸出了【INSTANCE】,是因為枚舉幫我們實現了toString,默認列印名稱。

public class EnumSingleton2Test{      public static void main(String[] args) {          HungryEnumSingleton singleton2 = HungryEnumSingleton.INSTANCE;          System.out.println(singleton2);          singleton2.print();      }  }

 輸出結果

 

靜態程式碼塊

這種方式和上面的靜態常量/變數類似,只不過把new放到了靜態程式碼塊里,從簡潔程度上比不過第一種。但是把new放在static程式碼塊有別的好處,那就是可以做一些別的操作,如初始化一些變數,從配置文件讀一些數據等。

/**   * 惡漢模式-靜態程式碼塊   */  public class HungryStaticBlockSingleton{        //構造器私有化      private HungryStaticBlockSingleton() {      }        //靜態變數保存實例變數      public static final HungryStaticBlockSingleton INSTANCE;        static {          INSTANCE = new HungryStaticBlockSingleton();      }  }

 

如下,在static程式碼塊里讀取 info.properties 配置文件動態配置的屬性,賦值給 info 欄位。

/**   * 惡漢模式-靜態程式碼塊   * 這種用於可以在靜態程式碼塊進行一些初始化   */  public class HungryStaticBlockSingleton{        private String info;        private HungryStaticBlockSingleton(String info) {          this.info = info;      }        //構造器私有化      private HungryStaticBlockSingleton() {      }        //靜態變數保存實例變數      public static final HungryStaticBlockSingleton INSTANCE;        static {          Properties properties = new Properties();          try {              properties.load(HungryStaticBlockSingleton.class.getClassLoader().getResourceAsStream("info.properties"));          } catch (IOException e) {              e.printStackTrace();          }          INSTANCE = new HungryStaticBlockSingleton(properties.getProperty("info"));      }      public String getInfo() {          return info;      }        public void setInfo(String info) {          this.info = info;      }  }

 

Test,

public class HungrySingletonTest{      public static void main(String[] args) {          HungryStaticBlockSingleton hun = HungryStaticBlockSingleton.INSTANCE;          System.out.println(hun.getInfo());      }  }

 

輸出

 

懶漢模式

需要時再創建,關鍵在於“懶”,類似懶載入。

非執行緒安全

同樣是構造方法私有化,提供給外部獲得實例的方法,getInstance()方法被調用時創建實例。該方式適用於單執行緒,因為在多執行緒的情況下可能會發生執行緒安全問題,導致創建不同實例的情況發生。可以看下面的演示。

 1 /**   2  * 懶漢模式-執行緒不安全的,適用於單執行緒   3  */   4 public class LazyUnsafeSingleton{   5     private LazyUnsafeSingleton(){   6     }   7     private static LazyUnsafeSingleton instance;   8     public static LazyUnsafeSingleton getInstance(){   9         if(instance==null){  10             instance = new LazyUnsafeSingleton();  11         }  12         return instance;  13     }  14 }

非執行緒安全演示

 1 public class LazyUnsafeSingletionTest{   2     public static void main(String[] args) throws ExecutionException, InterruptedException {   3         ExecutorService es = Executors.newFixedThreadPool(2);   4         Callable<LazyUnsafeSingleton> c1 = new Callable<LazyUnsafeSingleton>(){   5             @Override   6             public LazyUnsafeSingleton call() throws Exception {   7                 return LazyUnsafeSingleton.getInstance();   8             }   9         };  10         Callable<LazyUnsafeSingleton> c2 = new Callable<LazyUnsafeSingleton>(){  11             @Override  12             public LazyUnsafeSingleton call() throws Exception {  13                 return LazyUnsafeSingleton.getInstance();  14             }  15         };  16         Future<LazyUnsafeSingleton> submit = es.submit(c1);  17         Future<LazyUnsafeSingleton> submit1 = es.submit(c2);  18         LazyUnsafeSingleton lazyUnsafeSingleton = submit.get();  19         LazyUnsafeSingleton lazyUnsafeSingleton1 = submit1.get();  20         es.shutdown();  21  22         System.out.println(lazyUnsafeSingleton);  23         System.out.println(lazyUnsafeSingleton);  24         System.out.println(lazyUnsafeSingleton1==lazyUnsafeSingleton);  25     }  26 }

 

輸出 大概運行三次就會出現一次,我們可以在 LazyUnsafeSingleton 中判斷 if(instance==null) 之後增加執行緒休眠以獲得更好的效果。

執行緒安全的雙重檢查鎖模式

該方式是懶漢模式中執行緒安全的創建方式。通過同步程式碼塊控制並發創建實例。並且採用雙重檢驗,當兩個執行緒同時執行第一個判空時,都滿足的情況下,都會進來,然後去爭鎖,假設執行緒1拿到了鎖,執行同步程式碼塊的內容,創建了實例並返回,此時執行緒2又獲得鎖,執行同步程式碼塊內的程式碼,因為此時執行緒1已經創建了,所以執行緒2雖然拿到鎖了,如果內部不加判空的話,執行緒2會再new一次,導致兩個執行緒獲得的不是同一個實例。執行緒安全的控制其實是內部判空在起作用,至於為什麼要加外面的判空下面會說。

/**   * 懶漢模式-執行緒安全,適用於多執行緒   */  public class LazySafeSingleton{      private static volatile LazySafeSingleton safeSingleton;//防止指令重排      private LazySafeSingleton() {      }      public static LazySafeSingleton getInstance(){          if(safeSingleton==null){              synchronized (LazySafeSingleton.class){                  if(safeSingleton==null){//雙重檢測                      safeSingleton = new LazySafeSingleton();                  }              }            }          return safeSingleton;      }  }

 當不加內層判空時,會出現不是單例的情況,只不過出現的概率更低了點。

可不可以只加內層判空呢?答案是可以。

那為什麼還要加外層判空的呢?內層判空已經可以滿足執行緒安全了,加外層判空的目的是為了提高效率。因為可能存在這樣的情況:執行緒1拿到鎖後執行同步程式碼塊,在new之後,還沒有釋放鎖的時候,執行緒2過來了,它在等待鎖(此時執行緒1已經創建了實例,只不過還沒釋放鎖,執行緒2就來了),然後執行緒1釋放鎖後,執行緒2拿到鎖,進入同步程式碼塊匯總,判空,返回。這種情況執行緒2是不是不用去等待鎖了?所以在外層又加了一個判空就是為了防止這種情況,執行緒2過來後先判空,不為空就不用去等待鎖了,這樣提高了效率。

雙重檢查鎖模式是一種非常好的單例實現模式,解決了單例、性能、執行緒安全問題,上面的雙重檢測鎖模式看上去完美無缺,其實是存在問題,在多執行緒的情況下,可能會出現空指針問題,出現問題的原因是JVM在實例化對象的時候會進行優化和指令重排序操作。什麼是指令重排?

上面的safeSingleton = new LazySafeSingleton();操作並不是一個原子指令,會被分割成多個指令:

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

經過指令重排

1 memory = allocate(); //1:分配對象的記憶體空間  2 instance = memory; //3:設置instance指向剛分配的記憶體地址,此時對象還沒被初始化  3 ctorInstance(memory); //2:初始化對象

若有A執行緒進行完重排後的第二步,且未執行初始化對象。此時B執行緒來取singletonTest時,發現singletonTest不為空,於是便返回該值,但由於沒有初始化完該對象,此時返回的對象是有問題的。這也就是為什麼說看似穩的一逼的程式碼,實則不堪一擊。 

上述程式碼的改進方法:將safeSingleton聲明為volatile類型即可(volatile有記憶體屏障的功能)。

private static volatile LazySafeSingleton safeSingleton;

 

內部類創建外部類實例

該方式天然執行緒安全,是否final根據自己需要。

 1 /**   2  * 懶漢模式-執行緒安全,適用於多執行緒   3  * 在內部類被載入和初始化時 才創建實例   4  * 靜態內部類不會自動隨著外部類的載入和初始化而初始化,它是要單獨載入和初始化的。   5  * 因為是在內部類載入和初始化時創建的 因此它是執行緒安全的   6  */   7 public class LazyInnerSingleton{   8     private LazyInnerSingleton() {   9     }  10     private static class Inner{  11         private static final LazyInnerSingleton INSTANCE = new LazyInnerSingleton();  12     }  13     public static LazyInnerSingleton getInstance(){  14         return Inner.INSTANCE;  15     }  16 }

 

破壞單例模式的方法及解決辦法

1、除枚舉方式外, 其他方法都會通過反射的方式破壞單例,反射是通過調用構造方法生成新的對象,所以如果我們想要阻止單例破壞,可以在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法如下:

1 private SingletonObject(){  2     if (instance != null){  3         throw new RuntimeException("實例已經存在,請通過 getInstance()方法獲取");  4     }  5 }

2、如果單例類實現了序列化介面Serializable, 就可以通過反序列化破壞單例,所以我們可以不實現序列化介面,如果非得實現序列化介面,可以重寫反序列化方法readResolve(), 反序列化時直接返回相關單例對象。

1   public Object readResolve() throws ObjectStreamException {  2         return instance;  3    }

總結

餓漢模式

  • 靜態常量 簡潔直觀容易理解
  • 枚舉 最簡潔
  • 靜態程式碼塊 可以在靜態塊里做一些初始化的工作

懶漢模式

  • 單執行緒形式 該形式下不適用多執行緒,存在執行緒安全問題
  • 多執行緒形式 適用於多執行緒
  • 內部類形式 最簡潔

 
如果你覺得文章不錯,歡迎點贊轉發