單例模式深度解析:從餓漢式到枚舉實現的全方位解讀
單例設計模式概念
就是采取一定的方法保證在整個的軟件系統中,對某個類只能存在一個對象實例,并且該類只提供一個取得其對象實例的方法。如果我們要讓類在一個虛擬機中只能產生一個對象,我們首先必須將類的構造器的訪問權限設置為private,這樣,就不能用new操作符在類的外部產生類的對象了,但在類內部仍可以產生該類的對象。因為在類的外部開始還無法得到類的對象,只能調用該類的某個靜態方法以返回類內部創建的對象,靜態方法只能訪問類中的靜態成員變量,所以,指向類內部產生的該類對象的變量也必須定義成靜態的。
餓漢式
class Singleton {
// 1.私有化構造器
private Singleton() {
}
// 2.內部提供一個當前類的實例
// 4.此實例也必須靜態化
private static Singleton single = new Singleton();
// 3.提供公共的靜態的方法,返回當前類的對象;在內存中自始至終都存在
public static Singleton getInstance() {
return single;
}
}
案例:
public static void main(String[] args) {
User user1 = User.getUser();
System.out.println(user1);
User user2 = User.getUser();
System.out.println(user2);
}
class User{
//1、私有化構造器
private User() {
}
//2、內部提供一個當前類的實例,此實例也必須靜態化
private static User user = new User();
//3、提供公共的靜態的方法,返回當前類的對象;在內存中自始至終都存在
public static User getUser() {
return user;
}
}
//結果是一樣的,即同一個對象
com.gupao.singleton.User@6d6f6e28
com.gupao.singleton.User@6d6f6e28
static變量在類加載的時候初始化,此時不會涉及到多個線程對象訪問該對象的問題,虛擬機保證只會裝載一次該類,肯定不會發生并發問題,無需使用synchronized 關鍵字
存在的問題:如果只是加載了本類,而并不需要調用getUser,則會造成資源的浪費。
總結:線程安全、非懶加載、效率高,資源浪費
懶漢式
延遲對象的創建
方式1:普通創建
public class Singleton {
//私有構造方法
private Singleton() {}
//在成員位置創建該類的對象
private static Singleton instance;
//對外提供靜態方法獲取該對象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果是多線程環境,以上代碼會出現線程安全問題。
方式2:方法加鎖
class Singleton {
// 1.私有化構造器
private Singleton() {
}
// 2.內部提供一個當前類的實例
// 4.此實例也必須靜態化
private static Singleton instance;
// 3.提供公共的靜態的方法,返回當前類的對象
public static synchronized Singleton getInstance() {//注意多線程情況
if(instance== null) {
instance= new Singleton();
}
return instance;
}
}
以上使用同步方法會造成每次獲取實例的線程都要等鎖,會對系統性能造成影響,未能完全發揮系統性能,可使用同步代碼塊來解決
方式3:雙重檢查鎖
對于 getInstance() 方法來說,絕大部分的操作都是讀操作,讀操作是線程安全的,所以我們沒必讓每個線程必須持有鎖才能調用該方法,我們需要調整加鎖的時機。由此也產生了一種新的實現模式:雙重檢查鎖模式
public class Singleton {
//私有構造方法
private Singleton() {}
private volatile static Singleton instance;
//對外提供靜態方法獲取該對象
public static Singleton getInstance() {
//第一次判斷,如果instance不為null,不進入搶鎖階段,直接返回實例
if(instance == null) { // ①
synchronized (Singleton.class) {
//搶到鎖之后再次判斷是否為null
if(instance == null) {
instance = new Singleton();// ②
}
}
}
return instance;
}
}
為什么判斷兩次instance==null
第一次判斷是在代碼塊前,第二次是進入代碼塊后,第二個判斷想必都知道,多個線程都堵到代碼塊前等待鎖的釋放,進入代碼塊后要獲取到最新的instance值,如果為空就進行創建對象。那么為什么還要進行第一個判斷,第一個判斷起到優化作用,假設如果instance已經不為空了,那么沒有第一個判斷仍然會有線程堵在代碼塊前等待進一步判斷,所以如果不為空,有了第一個判斷就不用再去進入代碼塊進行判斷,也就不用再去等鎖了,直接返回。
為什么要加volatile?
- 是為了防止指令重排序,給私有變量加 volatile 主要是為了防止第 ② 處執行時,也就是“instance = new Singleton()”執行時的指令重排序的,這行代碼看似只是一個創建對象的過程,然而它的實際執行卻分為以下 3 步:
試想一下,如果不加 volatile,那么線程A在執行到上述代碼的第 ② 處時就可能會執行指令重排序,將原本是 1、2、3 的執行順序,重排為 1、3、2。但是特殊情況下,線程 A在執行完第 3 步之后,如果來了線程 B執行到上述代碼的第 ① 處,判斷 instance 對象已經不為 null,但此時線程 A還未將對象實例化完,那么線程B將會得到一個被實例化“一半”的對象,從而導致程序執行出錯,這就是為什么要給私有變量添加 volatile 的原因了。
- 創建內存空間。
- 在內存空間中初始化對象 Singleton。
- 將內存地址賦值給 instance 對象(執行了此步驟,instance 就不等于 null 了)。
- 優化作用,synchronized塊只有執行完才會同步到主內存,那么比如說instance剛創建完成,不為空,但還沒有跳出synchronized塊,此時又有10000個線程調用方法,那么如果沒有volatile,此使instance在主內存中仍然為空,這一萬個線程仍然要通過第一次判斷,進入代碼塊前進行等待,正是有了volatile,一旦instance改變,那么便會同步到主內存,即使沒有出synchronized塊,instance仍然同步到了主內存,通過不了第一個判斷也就避免了新加的10000個線程進入去爭取鎖。
總結:線程安全、懶加載、效率高。
靜態內部類(延遲初始化占位類)
靜態內部類單例模式中實例由內部類創建,由于 JVM 在加載外部類的過程中, 是不會加載靜態內部類的, 只有內部類的屬性/方法被調用時才會被加載, 并初始化其靜態屬性。靜態屬性由于被 static 修飾,保證只被實例化一次,并且嚴格保證實例化順序。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder{
private static final Singleton Instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.Instance;
}
}
第一次加載Singleton類時不會去初始化INSTANCE,只有第一次調用getInstance,虛擬機加載SingletonHolder并初始化INSTANCE,這樣不僅能確保線程安全,也能保證 Singleton 類的唯一性。
靜態內部類單例模式是一種優秀的單例模式,是開源項目中比較常用的一種單例模式。在沒有加任何鎖的情況下,保證了多線程下的安全,并且沒有任何性能影響和空間的浪費。
總結:線程安全、懶加載、效率高。
枚舉
枚舉類實現單例模式是極力推薦的單例實現模式,因為枚舉類型是線程安全的,并且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現單例模式,枚舉的寫法非常簡單,而且枚舉類型是所用單例實現中唯一一種不會被破壞的單例實現模式。
public enum Singleton {
INSTANCE;
}
提供了序列化機制,保證線程安全,絕對防止多次實例化,即使是在面對復雜的序列化或者反射攻擊的時候。
枚舉方式屬于餓漢式方式,會浪費資源
總結:線程安全、非懶加載、效率高。
幾種方式對比
方式 | 優點 | 缺點 |
餓漢式 | 線程安全、效率高 | 非懶加載,資源浪費 |
懶漢式synchronized方法 | 線程安全、懶加載 | 效率低 |
懶漢式雙重檢測 | 線程安全、懶加載、效率高 | 無 |
靜態內部類 | 線程安全、懶加載、效率高 | 無 |
枚舉 | 線程安全、效率高 | 非懶加載,資源浪費 |