1. 程式人生 > >單例模式的幾種實現方式及對比

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

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

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

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

餓漢模式

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

靜態常量

這裡將類的構造器私有化,就不能在外部通過new關鍵字建立該類的例項,然後定義了一個該類的常量,用static修飾,以便外部能夠獲得該類例項(通過HungryStaticConstantSingleton.INSTANCE

獲得)。也可以不加final關鍵字,具體看自己的需求。

 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