也來談談懶漢和餓漢,詳細解析單例模式的六種實現方式!

這是我參與8月更文挑戰的第5天,活動詳情查看:8月更文挑戰java

單例模式

  • 單例模式是一種建立模式,單例類負責本身建立本身的對象而且一個類只有一個實例對象,而且向整個系統提供這個實例.系統能夠直接訪問這個實例而不須要實例化
  • 單例模式的特色:
    • 單例類只有一個實例
    • 單例類必須本身建立自身的惟一實例
    • 單例類必須給其他系統對象提供建立的惟一實例

單例模式的實現方式

  • 單例模式要保證一個類只有一個實例,而且提供給全局訪問,主要用於解決一個全局使用的類頻繁建立和銷燬的問題,經過判斷系統是否存在這個單例來解決這樣的問題,若是有這個單例則返回這個單例,不然就建立這個單例,只要保證構造函數是私有的便可
    • 保證一個類只有一個實例: 將該類的構造方法定義爲私有方法便可
    • 提供全局一個該實例的訪問點: 單例類本身建立實例,提供一個靜態方法做爲實例的訪問點便可
  • 餓漢和懶漢比較:
    • 懶漢: 單例類對象實例懶加載,不會提早建立對象實例,只有在使用對象實例的時候纔會建立對象實例
    • 餓漢: 在單例對象實例進行聲明引用時就進行實例化建立對象實例
  • 單例模式除去線程不安全的懶漢,一般有五種實現方式:
    • 懶漢
    • 雙檢鎖
    • 餓漢
    • 靜態內部類
    • 枚舉
  • 通常狀況下,直接使用餓漢實現單例模式
  • 若是明確要求懶加載一般使用靜態內部類實現單例模式
  • 若是有關於反序列化建立對象會考慮使用枚舉實現單例模式
  • 靜態類Static :
    • 靜態類在第一次運行時直接初始化,也不須要在延遲加載中使用
    • 在不須要維持任何狀態,僅僅用於全局訪問時,使用靜態類的方式更加方便
    • 若是須要被繼承或者須要維持一些特定狀態下的狀況,就適合使用單例模式

線程不安全懶漢

線程安全懶漢

  • 單例模式線程安全懶漢Singleton示例
  • 解決了多線程環境下建立多個實例的問題
  • 存在每次獲取實例都須要申請鎖的問題,方法效率低下,由於在任什麼時候候只能有一個線程能夠調用getInstance() 方法

雙檢鎖

  • 雙重檢查鎖模式: doule checked locking pattern
    • 使用同步塊加鎖的方法
    • 會有兩次檢查instance == null
      • 一次在同步塊外
      • 一次在同步塊內
        • 由於會有多個線程一塊兒進入同步塊外的if
        • 若是不在同步塊內不進行二次檢驗就會致使生成多個實例
  • 單例模式雙檢鎖Singleton示例
  • volatile:
    • 對於計算機中的指令而言 ,CPU和編譯器爲了提高程序的執行效率,一般會按照必定的規則對指令進行優化
    • 若是兩條指令互不依賴,那麼指令執行的順序可能不是源碼的編寫順序
    • 形如instance = new Instance() 方法建立實例執行分爲三步:
      • 分配對象內存空間: 給新建立的Instance對象分配內存
      • 初始化對象: 調用單例類的構造函數來初始化成員變量
      • 設置instance指向新建立的對象分配的內存地址,此時instance != null
        • 由於上面的初始化對象和設置instance指向新建立的對象分配的內存地址不存在數據上的依賴關係,不管哪一步先執行都不會影響最終結果,因此程序在編譯時,順序就會發生改變:
          • 分配對象內存空間
          • 設置instance指向新建立對象分配的內存地址
          • 初始化對象
        • CPU和編譯器在指令重排時,不會關心指令重排執行是否影響多線程的執行結果. 若是不加volatile關鍵字,若是有多個線程訪問getInstance() 方法時,若是恰好發生了指令重排,可能會出現如下狀況:
          • 當第一個線程獲取鎖而且進入到第二個if方法後,先分配內存空間,而後instance指向剛剛分配的內存地址,此時instance不等於null. 可是此時instance尚未初始化完成
          • 若是此時有另外一個線程調用getInstance() 方法,在第一個if的判斷時結果就爲false, 就會直接返回沒有初始化完成的instance, 這樣可能會致使程序NPE異常
    • 使用volatile的緣由是禁止指令從新排序:
      • volatile變量進行賦值操做後會有一個內存隔離
      • 讀操做不會重排序到內存隔離之中
      • 好比在上面操做中,讀操做必須在執行完1,2,3或者1,3,2步驟以後纔會執行讀取到結果,不然不會讀取到相關結果

餓漢

  • 單例模式餓漢Singleton示例
  • 優勢:
    • 在單例類中,裝載類的時候就建立對象實例.由於單例類的實例聲明爲staticfinal變量,在第一次加在類到內存中時就會初始化,因此建立實例自己時線程安全的
  • 缺點:
    • 餓漢模式不是一種懶加載模式,即使客戶端沒有調用getInstance() 方法,單例類也會在類第一次加載時初始化
    • 使用餓漢模式建立單例類實例在某些場景中沒法使用:
      • 好比由於餓漢建立的實例聲明爲final變量
      • 若是單例類Singleton的實例的建立依賴參數或者配置文件
      • 須要在getInstance() 方法以前調用方法爲單例類的實例設置參數,此時這種餓漢模式就沒法使用

靜態內部類

  • 單例模式靜態內部類Singleton示例
  • 使用靜態內部類模式建立單例類實例是使用JVM機制保證線程安全:
    • 靜態單例對象沒有做爲單例類的成員變量直接實例化,因此當類加載時不會實例化單例類
    • 第一次調用getInstance() 方法時將加載靜態內部類Nest. 在靜態內部類中定義了一個static類型的變量instance, 這時會首先初始化這個變量
    • 經過JVM來保證線程安全,確保該成員變量只初始化一次
    • 因爲getInstance() 方法並無加線程鎖,因此對性能沒有什麼影響
  • 靜態內部類的優勢:
    • 靜態內部類Nest是私有的,只能經過getInstance() 方法進行訪問,因此這是懶加載的
    • 讀取實例時不會進行同步鎖的獲取,性能較好
    • 靜態內部類不依賴JDK版本

枚舉

  • 單例模式枚舉Singleton示例
  • 使用枚舉方式實現單例的最大特色是很是簡單
  • 能夠經過Enum.INSTANCE來訪問實例,和getInstance() 方法比較更加簡單
  • 枚舉的建立默認就是線程安全的方法,並且能防止反射以及反序列化致使從新建立新的對象
    • Enum類內部使用Enum類型斷定防止經過反射建立新的對象
    • Enum類經過對象的類型和枚舉名稱將對象進行序列化,而後經過valueOf() 方法匹配枚舉名稱找到內存中的惟一對象實例,這樣能夠防止反序列化時建立新的對象
  • 懶漢式和餓漢式實現的單例模式破壞 : 不管是經過懶漢式仍是餓漢式實現的單例模式,均可能經過反射和反序列化破壞掉單例的特性,能夠建立多個對象
  • 反射破壞單例模式: 利用反射,能夠強制訪問單例類的私有構造器,建立新的對象
public static void main(String[] args) {
	// 利用反射獲取單例類的構造器
	Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
	// 設置訪問私有構造器
 	constructor.setAccessiable(true);
 	// 利用反射建立新的對象
 	Singleton newInstance = constructor.newInstance();
 	// 經過單例模式建立單例對象
 	Singleton singletonInstance = Singleton.getInstance();
 	// 此時這兩個對象是兩個不一樣的對象,返回false
 	System.out.println(singletonInstance  == newInstance);
}
複製代碼
  • 反序列化破壞單例模式: 經過readObject() 方法讀取對象時會返回一個新的對象實例
public static void main(String[] args) {
	// 建立一個輸出流對象
	ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
	// 將單例類對象寫入到文件中
	Singleton singletonInstance = Singleton.getInstance();
	os.writeObject(singleton);
	// 從文件中讀取單例對象
	File file = new File("Singleton.file");
	ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));
	Singleton newInstance = (Singleton)is.readObject();
	// 此時這兩個對象是兩個不一樣的對象,返回false
	System.out.println(singletonInstance == newInstance);
}
複製代碼
相關文章
相關標籤/搜索