單例模式裏面其實也是包含很多知識點的,整理一下有助於知識的融會貫通。
單例模式的解決的痛點就是節約資源,節省時間從兩個方面看:
1.由於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級的對象而言,是很重要的.
2.因爲不需要頻繁創建對象,我們的GC壓力也減輕了,而在GC中會有STW(stop the world),從這一方面也節約了GC的時間 單例模式的缺點:簡單的單例模式設計開發都比較簡單,但是複雜的單例模式需要考慮線程安全等併發問題,引入了部分複雜度。
線程安全 | 併發性能好 | 可以延遲加載 | 序列化/反序列化安全 | 能抵禦反射攻擊 | |
---|---|---|---|---|---|
餓漢式 | Y | Y | |||
懶漢不加鎖 | Y | Y | |||
懶漢加鎖的 | Y | Y | |||
DCL | Y | Y | Y | ||
靜態內部類 | Y | Y | Y | ||
枚舉 | Y | Y | Y | Y |
懶漢式
懶漢式就是他很懶,只有在用的時候才進行實例化。
1. 單線程
public class Singleton {
private static Singleton singleton;
// 私有的構造方法,不能在外部類實例化
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
但是上面這個方法在多線程的情況下就不行了。如果多個線程同時運行到判空的地方。而且單例的確沒有被創建。那麼兩個線程都會創建一個單例。那此時就不是單例了。
2. 直接使用synchronized但是效率低
public class Singleton {
private static Singleton singleton;
// 私有的構造方法,不能在外部類實例化
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
這樣確實可以保證線程安全,但是加鎖釋放鎖是比較耗費性能的。所以這個方法不推薦。
3. 雙重檢驗
public class Singleton {
private static Singleton singleton;
// 私有的構造方法,不能在外部類實例化
private Singleton() {
}
public static Singleton getInstance() {
// 線程併發問題只有在初始化單例的時候會出現,所以如果實例存在那麼就不用加鎖了。
if (singleton == null) {
synchronized (Singleton.class) {
// 實例不存在的時候,先獲取鎖,但是此時有可能其他線程已經實例化過了。所以再判空。
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4. 加volatile
public class Singleton {
// 這個volatile很關鍵,下面細講
private volatile static Singleton singleton;
// 私有的構造方法,不能在外部類實例化
private Singleton() {
}
public static Singleton getInstance() {
// 線程併發問題只有在初始化單例的時候會出現,所以如果實例存在那麼就不用加鎖了。
if (singleton == null) {
synchronized (Singleton.class) {
// 實例不存在的時候,先獲取鎖,但是此時有可能其他線程已經實例化過了。所以再判空。
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
爲什麼需要volatile呢?
我們都知道volatile有內存可見性和防止指令重排序的功能。
首先創建對象分爲三個步驟:
- 分配內存空間
- 初始化對象
- 將內存空間的地址賦值給對象的引用。
但是JVM可能會對代碼進行重排序,所以真正的執行順序可能是1->3->2。
那麼當第一個線程搶到鎖執行初始化對象的時候,發生了重排序,這個時候對象還沒初始化,但是對象的引用已經不爲空了。
當第二個線程遇到第一個判空時,就會直接返回對象,但是第一個線程此時還沒執行完初始化對象,就會造成第二個線程拿到的是一個空對象。造成空指針問題。
5. 靜態內部類方法
public class Singleton {
// 私有的構造方法,不能在外部類實例化
private Singleton() {
}
public static Singleton getInstance() {
return StaticSingletonHolder.singelton;
}
// 一個私有的靜態內部類,用於初始化一個靜態final實例
private static class StaticSingletonHolder {
private static final Singleton singleton = new Singleton();
}
}
加載一個類時,其中內部類不會同時被加載。一個類被加載,僅僅當某個靜態成員被調用時發生。由於在調用 StaticSingleton.getInstance() 的時候,纔會對單例進行初始化,而且通過反射,是不能從外部類獲取內部類的屬性的;由於靜態內部類的特性,只有在其被第一次引用的時候纔會被加載,所以可以保證其線程安全性。
優勢:兼顧了懶漢模式的內存優化(使用時才初始化)以及餓漢模式的安全性(不會被反射入侵)。
劣勢:需要兩個類去做到這一點,雖然不會創建靜態內部類的對象,但是其 Class 對象還是會被創建,而且是屬於永久代的對象。
餓漢式
1. 餓漢式
public class Singleton {
// 這個volatile很關鍵,下面細講
private static final Singleton singleton = new Singleton();
// 私有的構造方法,不能在外部類實例化
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
在類初始化時,已經自行實例化。
instance什麼時候被初始化?
Singleton類被加載的時候就會被初始化,java虛擬機規範雖然沒有強制性約束在什麼時候開始類加載過程,但是對於類的初始化,虛擬機規範則嚴格規定了有且只有四種情況必須立即對類進行初始化,遇到new、getStatic、putStatic或invokeStatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。 生成這4條指令最常見的java代碼場景是:
1)使用new關鍵字實例化對象
2)讀取一個類的靜態字段(被final修飾、已在編譯期把結果放在常量池的靜態字段除外)
3)設置一個類的靜態字段(被final修飾、已在編譯期把結果放在常量池的靜態字段除外)
4)調用一個類的靜態方法
class的生命週期?
class的生命週期一般來說會經歷加載、連接、初始化、使用、和卸載五個階段
class的加載機制
這裏可以聊下classloader的雙親委派模型。
2. 枚舉方法
枚舉默認就是線程安全的,所以不需要擔心DCL。但能防止反序列化導致重新創建新的對象。即使使用反射機制也無法實例化一個枚舉量。
public class Singleton {
enum Single {
SINGLE;
private Single() {
}
}
}