單例模式的幾種實現方式及對比
所謂單例就是在系統中只有一個該類的例項。
單例模式的核心分以下三個步驟:
- 構造方法私有化。即不能在類外例項化,只能在類內例項化。
- 在本類中建立本類的例項。
- 在本類中提供給外部獲取例項的方式。
單例模式的實現方式有兩種:餓漢模式和懶漢模式。
餓漢模式
不管現在需不需要,先建立例項。關鍵在於“餓”,餓了就要立即吃。
靜態常量
這裡將類的構造器私有化,就不能在外部通過new關鍵字建立該類的例項,然後定義了一個該類的常量,用static修飾,以便外部能夠獲得該類例項(通過HungryStaticConstantSingleton.INSTANCE
1 /** 2 * 惡漢模式-靜態常量,簡潔直觀 3 */ 4 public class HungryStaticConstantSingleton{ 5 //構造器私有化 6 private HungryStaticConstantSingleton() { 7 } 8 //靜態變數儲存例項變數 並提供給外部例項 9 public final static HungryStaticConstantSingleton INSTANCE = new HungryStaticConstantSingleton(); 10 }
列舉
這種方式是最簡潔的,不需要考慮構造方法私有化。值得注意的是列舉類不允許被繼承,因為列舉類編譯後預設為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過來後先判空,不為空就不用去等待鎖了,這樣提高了效率。
內部類建立外部類例項
該方式天然執行緒安全,是否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 }
總結
餓漢模式
- 靜態常量 簡潔直觀容易理解
- 列舉 最簡潔
- 靜態程式碼塊 可以在靜態塊裡做一些初始化的工作
懶漢模式
- 單執行緒形式 該形式下不適用多執行緒,存線上程安全問題
- 多執行緒形式 適用於多執行緒
- 內部類形式 最簡潔
&n