這是我參與8月更文挑戰的第5天,活動詳情查看:8月更文挑戰java
單例模式
- 單例模式是一種建立模式,單例類負責本身建立本身的對象而且一個類只有一個實例對象,而且向整個系統提供這個實例.系統能夠直接訪問這個實例而不須要實例化
- 單例模式的特色:
- 單例類只有一個實例
- 單例類必須本身建立自身的惟一實例
- 單例類必須給其他系統對象提供建立的惟一實例
單例模式的實現方式
- 單例模式要保證一個類只有一個實例,而且提供給全局訪問,主要用於解決一個全局使用的類頻繁建立和銷燬的問題,經過判斷系統是否存在這個單例來解決這樣的問題,若是有這個單例則返回這個單例,不然就建立這個單例,只要保證構造函數是私有的便可
- 保證一個類只有一個實例: 將該類的構造方法定義爲私有方法便可
- 提供全局一個該實例的訪問點: 單例類本身建立實例,提供一個靜態方法做爲實例的訪問點便可
- 餓漢和懶漢比較:
- 懶漢: 單例類對象實例懶加載,不會提早建立對象實例,只有在使用對象實例的時候纔會建立對象實例
- 餓漢: 在單例對象實例進行聲明引用時就進行實例化建立對象實例
- 單例模式除去線程不安全的懶漢,一般有五種實現方式:
- 通常狀況下,直接使用餓漢實現單例模式
- 若是明確要求懶加載一般使用靜態內部類實現單例模式
- 若是有關於反序列化建立對象會考慮使用枚舉實現單例模式
- 靜態類Static :
- 靜態類在第一次運行時直接初始化,也不須要在延遲加載中使用
- 在不須要維持任何狀態,僅僅用於全局訪問時,使用靜態類的方式更加方便
- 若是須要被繼承或者須要維持一些特定狀態下的狀況,就適合使用單例模式
線程不安全懶漢
線程安全懶漢
雙檢鎖
- 雙重檢查鎖模式: 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示例
- 優勢:
- 在單例類中,裝載類的時候就建立對象實例.由於單例類的實例聲明爲static的final變量,在第一次加在類到內存中時就會初始化,因此建立實例自己時線程安全的
- 缺點:
- 餓漢模式不是一種懶加載模式,即使客戶端沒有調用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();
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();
System.out.println(singletonInstance == newInstance);
}
複製代碼