設計模式|完全理解單列模式

單例模式是一種經常使用的設計模式、也多是設計模式中代碼量最少的設計模式。可是少並不意味着簡單、想要用好、用對單例、就的費一番腦子了。由於它裏面涉及到了不少Java底層的知識如類裝載機制、Java內存模型、volatile等知識點。數據庫

簡介

單例模式屬於23中設計模式中的建立型模式、定義是確保某一個類只有一個實例、並提供一個全局的訪問點。設計模式

具備如下3個特性:
  • 只能有一個實例
  • 必須本身建立本身惟一實例
  • 提供全局訪問點

基本實現思路

單例要求類只能返回同一對象的引用、必須提供一個靜態獲取該實例的方法
實現能夠經過如下兩步:安全

  • 私有化構造方法、防止外部實例化、只有經過對外提供的靜態方法來獲取惟一實例
  • 提供一個靜態方法獲取對象的實例。

單例的7種實現方式

1. 餓漢式
public class EagetSingleton {

    private static final EagetSingleton INSANCE = new EagetSingleton();

    // 私有化構造函數、防止外部實例化
    private EagetSingleton() {
    }

    // 提供靜態外部訪問方法
    public static EagetSingleton getInstance() {
        return INSANCE;
    }

}

優勢:寫法簡單、類裝載時就實例化了靜態變量、避免了線程併發問題。
缺點:在類裝載過程當中就實例化了對象、形成了資源浪費。多線程

2. 餓漢式(靜態代碼塊)
public class StaticBlockSingleton {

    private static StaticBlockSingleton INSTANCE = null;

    static {
        try {
            INSTANCE = new StaticBlockSingleton();
        } catch (Exception e) {
        }
    }

    // 私有化構造函數、防止外部實例化
    private StaticBlockSingleton() {
    }

    // 提供靜態外部訪問方法
    public static StaticBlockSingleton getInstance() {
        return INSTANCE;
    }

}

這種方式和上述實現方式基本相同、只是把類實例化的過程放到了靜態代碼塊中來實例化、一樣也是在類裝載過程執行靜態代碼塊、優缺點基本相同可是它能夠在類實例化過程當中作一些額外的操做如異常處理等。併發

3. 懶漢式(線程不安全)
public class LazySingleton {

    private static LazySingleton INSTANCE = null;

    // 私有化構造函數、防止外部實例化
    private LazySingleton() {
    }

    // 提供靜態外部訪問方法
    public static LazySingleton getInstance() {
        if (null == INSTANCE) { -------- 1
            INSTANCE = new LazySingleton(); ------2
        }
        return INSTANCE;
    }

}

優勢:實現了懶加載、避免了資源的浪費。
缺點:線程不安全、在多線程狀況下當一個線程執行到 1 處的時候、尚未來得及往下執行另外一個線程也到 1 處 這樣兩個線程同時執行 2 處代碼、破壞了單例。函數

4. 懶漢式(加鎖)
public class LazySyncSingleton {

    private static LazySyncSingleton INSTANCE = null;

    // 私有化構造函數、防止外部實例化
    private LazySyncSingleton() {
    }

    // 效率低下
    // 提供靜態外部訪問方法
    public static synchronized LazySyncSingleton getInstance() {

        if (null == INSTANCE) {
            INSTANCE = new LazySyncSingleton();
        }

        return INSTANCE;
    }
}

解決了3中線程不安全的問題、利用synchronized對getInstance()方法加鎖以達到同步訪問。
優勢:線程同步
缺點:效率低下、此方式對整個對象加鎖、每次訪問getInstance() 都須要同步訪問、這種狀況多線程併發效率很是低下、其實咱們只須要在對象還沒實例化前加鎖就能夠了、實例化後就不存在併發問題了。工具

5. 懶漢式(雙重鎖)
public class DCheckSingleton {
    private static volatile DCheckSingleton INSTANCE = null;

    // 私有化構造函數、防止外部實例化
    private DCheckSingleton() {
    }

    // 提供靜態外部訪問方法
    public static DCheckSingleton getInstance() {

        if (null == INSTANCE) {
            synchronized (DCheckSingleton.class) {
                if (null == INSTANCE) {
                    INSTANCE = new DCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

解決了4中併發狀況下效率低下的問題。
優勢:線程安全、延遲加載、效率高
涉及到知識點: 1:volatile 關鍵字 確保內存的可見性和有序性。若是不加volatile關鍵字會有什麼狀況? 我知道在對象實例化時INSTANCE = new DCheckSingleton();這一句代碼JVM中並非一步執行的而是分爲三步(1)在棧內存中爲 建立對象的引用指針 INSTANCE (2)在堆內存中開闢一塊空間來存放實例化的對象 new DCheckSingleton(); (3)將INSTANCE指向堆內存空間地址J、VM只保證了代碼執行結果的正確性、並不保證執行順序(這裏涉及到Java內存模型知識點在這就很少說了、感興趣的同窗能夠去了解下JVM一些底層實現原理)因此 1 ,2,3三步也多是1 ,3 ,2 這樣咱們就可能拿到的時一個半成品的對象了。
2: 涉及到類實例化知識點
3: 涉及到Java內存模型
4:涉及到JVM的一些執行優化、指令重排等性能

6. 靜態類部類
public class InnerSingleton {

    private  InnerSingleton() {
    }
    
    public static InnerSingleton getInstance() {
        return InnerClassSingleton.INSTANCE;
    }
    
    private static class InnerClassSingleton{
        private static final InnerSingleton INSTANCE = new InnerSingleton();
    }
}

這種方式和餓漢式的實現機制基本相同、都是利用了類裝載機制來保證線程的安全、它和餓漢式的惟一區別就是實現了懶加載的機制、只有在調用getInstance()方法時纔去進行InnerClassSingleton類的實例化。
優勢:避免了線程不安全,延遲加載,效率高。測試

7. 枚舉
public enum EnumsSingleton {
    INSTANCE;

    @SuppressWarnings("unused")
    private void method() {
        System.out.println("------- newInstance");
    }

}

藉助JDK1.5中添加的枚舉來實現單例模式。不只能避免多線程同步問題,並且還能防止反序列化從新建立新的對象。多是由於枚舉在JDK1.5中才添加、因此在實際項目開發中、不多見人這麼寫過。優化

到這單例幾種實現方式以及每種方式的優缺點都作了一些簡單的介紹、枚舉雖小可是設計的知識點不少。

優勢

  1. 在單例模式中,活動的單例只有一個實例,對單例類的全部實例化獲得的都是相同的一個實例。這樣就 防止其它對象對本身的實例化,確保全部的對象都訪問一個實例
  2. 單例模式具備必定的伸縮性,類本身來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。
  3. 提供了對惟一實例的受控訪問。
  4. 因爲在系統內存中只存在一個對象,所以能夠 節約系統資源,當 須要頻繁建立和銷燬的對象時單例模式無疑能夠提升系統的性能。
  5. 容許可變數目的實例。
  6. 避免對共享資源的多重佔用

缺點

  1. 不適用於變化的對象,若是同一類型的對象老是要在不一樣的用例場景發 生變化,單例就會引發數據的錯誤,不能保存彼此的狀態。
  2. 因爲單利模式中沒有抽象層,所以單例類的擴展有很大的困難。
  3. 單例類的職責太重,在必定程度上違背了「單一職責原則」。
  4. 濫用單例將帶來一些負面問題,如爲了節省資源將數據庫鏈接池對象設 計爲的單例類,可能會致使共享鏈接池對象的程序過多而出現鏈接池溢 出;若是實例化的對象長時間不被利用,系統會認爲是垃圾而被回收, 這將致使對象狀態的丟失。

使用場景

  1. 須要頻繁的進行建立和銷燬的對象;
  2. 建立對象時耗時過多或耗費資源過多,但又常常用到的對象;
  3. 工具類對象;
  4. 頻繁訪問數據庫或文件的對象。

注意

最後在簡單聊一下如何防止暴力破壞單例。主要介紹兩種方式以及如何來防範這兩種方式。
1: 利用Java的反射方式

EagerSingleton instance = EagerSingleton.getInstance();
        Constructor instance2 = instance.getClass().getDeclaredConstructor();
         instance2.setAccessible(true);
         EagerSingleton instance3 = (EagerSingleton) instance2.newInstance();
         System.out.println("===" + instance);
         System.out.println("===" + instance3);

利用Java的反射方式能夠達到爆力破解單例的效果、運行結果我就不在這貼出了有興趣的能夠本身試試instance 和 instance3 確定不是一個對象。
如何來防範這方式? 其實也很簡單Java Security 中爲咱們提供了現成的方法。只須要在私有構造中使用SecurityManager 進行檢查下就能夠代碼以下。

// 私有的構造方法,防止外部實例化
     private EagerSingleton() {
         SecurityManager sm = new SecurityManager();
         sm.checkPermission(new ReflectPermission("禁止反射"));
     }

2: 第二種方式是利用Java 序列化和反序列化來實現

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.txt"));
         out.writeObject(instance);
         out.close();
         ObjectInputStream in = new ObjectInputStream(new FileInputStream("1.txt"));
         EagerSingleton readObject = (EagerSingleton) in.readObject();
         in.close();
         System.out.println("==" + instance);
         System.out.println("==" + readObject);

如何防範? 很簡單隻須要重寫readResolve() 反方就能夠了

private Object readResolve() {
         return EagerSingleton.instance;
     }

兩種暴力破解和防範的方式都介紹完了,感興趣的同志能夠去試試我這裏沒有貼出完整的測試代碼和運行結果。

~~~~~~到這咱們的小單例已經介紹完了,有沒有感到驚訝!!!

相關文章
相關標籤/搜索