單例模式的基礎實現方式
手寫普通的單例模式要點有三個:
- 將建構函式私有化
- 利用靜態變數來儲存全域性唯一的單例物件
- 使用靜態方法
getInstance()
獲取單例物件
懶漢模式
懶漢模式指的是單例物件的延遲載入,只有在呼叫 getInstance()
獲取單例物件時才會將單例建立出來。懶漢模式適用於對記憶體要求高的場景。程式碼如下:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
餓漢模式
與懶漢模式相對的是餓漢模式,適用於對記憶體要求不高的場景,在類載入的初始化階段就完成了單例物件的建立,程式碼如下:
public class Singleton {
// 靜態變數初始化
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
靜態變數的初始化是在類載入階段的初始化過程進行,在此期間,編譯器會自動收集類中所有靜態變數的賦值動作和 static
塊,生成 <clinit>
方法並執行。比較特殊的一點是,如果多個執行緒同時初始化 Singleton
類,JVM 會保證只有一個執行緒能夠執行 Singleton
類的 <clinit>
方法,其他執行緒都必須阻塞等待。而且同一個類載入器下,一個類只會被初始化一次,即 <clinit>
方法只會被執行一次,這就保證了多執行緒下單例物件只會被建立一次
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
多執行緒下的單例模式
單例模式需要保證的一點是,在整個程式執行期間,單例物件只會被建立一次。如果是單執行緒環境中,這一點很好保證。但如果是多執行緒環境中,保證這一點並不簡單
上面已經說過,餓漢模式的單例模式下,JVM 會保證單例物件只會被建立一次,因此可以保證這一點。而懶漢模式在多執行緒環境中不能保證這一點,接下來討論的是對懶漢模式進行改造,讓它能夠保證這一點
使用synchronized方法
最簡單直接的方式就是為 getInstance()
加上 synchronized
關鍵字,這樣確實可以保證多執行緒環境中,單例物件只會被建立一次。但是 synchronized
方法最大的缺點在於它將獲取單例物件這一行為徹底序列化,同一時刻只能有一個執行緒能執行 getInstance()
,大大降低了併發效率
程式碼如下:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
雙重檢測鎖
直接使用 synchronized
方法降低效率的主要原因在於,synchronized
方法的加鎖粒度太粗,那麼將鎖的範圍縮小,就可以緩解這一問題,而雙重檢測鎖就是這麼實現的。不過為了保證併發的正確性,在內部又加了一道檢測,故名為雙重檢測鎖。程式碼如下:
public class Singleton {
// 這裡的instance一定要定義為volatile變數!!!
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 雙重鎖檢測
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面程式碼的關鍵點有三個:
synchronized
加鎖的範圍更小,這是為了更高的併發效率synchronized
內部還有一道檢測,如果執行緒1進入了同步塊,但還未將單例物件建立出來,此時執行緒2正好繞過了第一道檢測,在同步塊外等待獲取鎖定。因此同步塊內也要加上一道檢測,避免單例物件被重複建立instance
這個變數一定要宣告為volatile
!volatile
在這裡最大的作用是禁止指令重排序。如果不加volatile
修飾,由於instance = new Singleton()
可能被重排序而導致在這條語句執行過程中,instance
率先被分配記憶體並獲得地址,成為非 null,但建構函式卻沒有真正執行完畢,此時別的執行緒可能拿到的instance
就是不完全構造的單例物件
instance = new Singleton()
這條語句正常的執行順序是:
1、為即將建立的物件分配一塊記憶體
2、執行建構函式中的語句,對記憶體進行相應的讀寫操作
3、讓 instance
指向這塊記憶體
在重排序情況下順序可能是 1 -> 3 -> 2,當執行到3時 instance
就成為非 null,此時其他執行緒如果引用了 instance
,拿到的就是一個不完全構造的物件
需要注意的是,在 JDK5 之前,就算加了
volatile
關鍵字也依然有問題,原因是之前的 JMM 是有缺陷,volatile
變數前後的程式碼仍然可以出現重排序問題,這個問題在 JDK5 之後才得到解決,所以現在才可以這麼使用
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
其他單例模式的實現方式
基於列舉類
基於列舉類的方式非常簡潔,只要簡單地編寫一個只包含一個元素的列舉類,由 JVM 來保證單例的唯一性和執行緒安全性,自帶私有的構造方法並且序列化和反射都不會破壞單例的唯一性,據說是 JDK5 之後最好的單例建立方式
public enum Singleton {
instance;
// 定義各種欄位、方法
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
其中列舉類的構造器不用特意加上 private
修飾,因為列舉類構造器預設就是 private
的,且只能使用 private
修飾
簡單理解列舉實現單例的過程:程式啟動時,會自動呼叫
Singleton
的構造器,例項化單例物件並賦給instance
,之後再也不會例項化,這也是一個餓漢過程,即使沒有呼叫過getInstance()
,也會將單例物件建立出來
使用列舉來建立單例模式的優勢有3點:
- 程式碼量更少,更加簡潔
- 沒有做任何額外的操作,就可以保證單例的唯一性和執行緒安全性
- 使用列舉類可以防止呼叫者使用反射、序列化和反序列化機制強制生成多個單例物件,破壞唯一性
這第三點優勢讓基於列舉類的單例模式變得“無懈可擊”了,列舉類可以保證唯一性的原理如下:
- 防反射
列舉類預設繼承了 Enum
類,在利用反射呼叫 newInstance()
時,會判斷該類是否是列舉類,如果是則丟擲異常
- 防反序列化建立多個列舉物件
對於列舉型別,由於列舉類和列舉變數的組合名是唯一的,可以唯一確定物件。因此,序列化只會將列舉類名 + 列舉變數名輸出到檔案中。反序列化時,讀入的就是列舉類名 + 列舉變數名,再根據 Enum
類的 valueOf
方法,在記憶體中找對已經存在的列舉物件,並不會建立新的物件
類載入器對單例模式的影響
同一個類載入器對一個類只會載入一次,但是不同的類載入器可能會多次載入同一個類,如果程式中有多個類載入器,需要在單例中指定某個特定的類載入器,並保證這個類載入器始終是同一個