一文帶你摸清設計模式之單例模式!

zfj0318發表於2022-06-04

本文將給大家介紹java中設計模式——單例模式;

單例模式:保證在⼀個JVM中,該物件只有⼀個例項存在;

適⽤場景:

1、某些類建立⽐較頻繁,對於⼀些⼤型的物件,這是⼀筆很⼤的系統開銷。

2、省去了new操作符,降低了系統記憶體的使⽤頻率,減輕GC壓⼒。

3、有些類如交易所的核⼼交易引擎,控制著交易流程,如果該類可以建立多個的話,系統完全亂了。(⽐如⼀個軍隊出現了多個司 令員同時指揮,肯定會亂成⼀團),所以只有使⽤單例模式,才能保證核⼼交易伺服器獨⽴控制整個流程。

 

程式碼:

 1 public class Singleton{
 2      //持有私有靜態例項,防止被引用,賦值為null,目的就是實現延遲載入
 3     private static Singleton instance = null;
 4 
 5 
 6     //私有構造方法,防止被例項化。
 7   private Singleton(){
 8      }
 9    
10   //建立例項
11   public static Singleton getInstance()
12 {
13       if (instance=null){
14       instance=new Singleton();
15        
16 }
17 return  instance;
18 //如果該物件被⽤於序列化,可以保證物件在序列化前後保持⼀致
19    public Object readReaslove(){
20    return instance;
21 }
22 
23 }
24 }

 

分類:

1.餓漢式:類初始化時建立單例,執行緒安全,但佔用記憶體空間大,適⽤於佔記憶體⼩的場景,否則推薦使⽤懶漢式延遲載入;

   public class Singleton{
private static Singleton instance = new Singleton();
//構造器私有, 外部就無法new這個物件,保證一個類中只有一個例項物件
private Singleton(){}
public static Singleton newInstance(){ return instance; } }

 

2.懶漢式:需要單例例項的時候再建立,需要考慮執行緒安全(效能不太好)

 
懶漢式單例如何保證執行緒安全呢?
通過 synchronized 關鍵字加鎖保證執行緒安全, synchronized 可以新增在⽅法上⾯,也可以新增在程式碼塊上⾯,這⾥演示新增在⽅法上⾯,存在的問題是 每⼀次調⽤ getInstance 獲取例項時都需要加鎖和釋放鎖,這樣是⾮常影響效能的。
 1 public class Singleton{
 2   private static Singleton instance = null;//定義一個靜態變數指向自己
 3   private Singleton(){}//私有化構造方法
 4   public static synchronized Singleton newInstance(){//加鎖,
 5       if (instance == null){
 6          instance = new Singleton();
 7 }
 8 }
 9     return instance;
10 
11 }

 

3.雙重檢驗鎖:假如兩個執行緒A、B,A執⾏了if (instance == null)語句,它會認為單例物件沒有建立,此時執行緒切到B也執⾏了同樣的語句,B也認為單例物件沒有建立,然後兩個執行緒依次執⾏同步程式碼塊,並分別建立了⼀個單例物件。

    這⾥的雙重檢查是指兩次⾮空判斷,鎖指的是 synchronized 加鎖,為什麼要進⾏雙重判斷,其實很簡單,第⼀重判斷,如果例項已經存在,那麼就不再需要進⾏同步操作,⽽是直接返回這個例項,如果沒有建立,才會進⼊同步塊,同步塊的⽬的與之前相同,⽬的是為了防⽌有多個執行緒同時調⽤時,導致⽣成多個例項,有了同步塊,每次只能有⼀個執行緒調⽤訪問同步塊內容,當第⼀個搶到鎖的調⽤獲取了例項之後,這個例項就會被建立,之後的所有調⽤都不會進⼊同步塊,直接在第⼀重判斷就返回了單例。
關於內部的第⼆重空判斷的作⽤,當多個執行緒⼀起到達鎖位置時,進⾏鎖競爭,其中⼀個執行緒獲取鎖,如果是第⼀次進⼊則為 null,會進⾏單例物件的建立,完成後釋放鎖,其他執行緒獲取鎖後就會被空判斷攔截,直接返回已建立的單例物件。其中最關鍵的⼀個點就是 volatile 關鍵字的使⽤,關於 volatile 的詳細介紹可以直接搜尋 volatile 關鍵字即可,有很多寫的⾮常好的⽂章,這⾥不做詳細介紹,簡單說明⼀下,雙重檢查鎖中使⽤ volatile 的兩個重要特性:可⻅性、禁⽌指令重排序
 
這⾥為什麼要使⽤ volatile ?
這是因為 new 關鍵字建立物件不是原⼦操作,建立⼀個物件會經歷下⾯的步驟:
1. 在堆記憶體開闢記憶體空間
2. 調⽤構造⽅法,初始化物件
3. 引⽤變數指向堆記憶體空間
 
程式碼:
 1 public class Singleton{
 2    private  static  volatile Singleton instance = null;
 3    private Singleton(){}
 4   public static Singleton getInstance(){
 5    if (instance == null)
 6   {     synchronized (Singleton.class){
 7            if (instance == null){
 8     instance = new Singleton();
 9 
10 }
11    }
12 }
13 return instance;
14 }

 

4.靜態內部類:可以同時保證延遲載入和執行緒安全。靜態內部類單例是如何實現懶載入的呢?(懶載入 :使⽤的時候再建立物件)⾸先,我們先了解下類的載入時機。

虛擬機器規範要求有且只有 5 種情況必須⽴即對類進⾏初始化(載入、驗證、準備需要在此之前開始):
1. 遇到 new 、 getstatic 、 putstatic 、 invokestatic 這 4 條位元組碼指令時。⽣成這 4 條指令最常⻅的 Java 程式碼場景是:使⽤ new 關鍵字例項化物件的時候、讀取或設定⼀個類的靜態欄位(final 修飾除外,被final 修飾的靜態欄位是常量,已在編譯期把結果放⼊常量池)的時候,以及調⽤⼀個類的靜態⽅法的時候。
2. 使⽤ java.lang.reflect 包⽅法對類進⾏反射調⽤的時候。
3. 當初始化⼀個類的時候,如果發現其⽗類還沒有進⾏過初始化,則需要先觸發其⽗類的初始化。
4. 當虛擬機器啟動時,⽤戶需要指定⼀個要執⾏的主類(包含 main()的那個類),虛擬機器會先初始化這個主類。
5. 當使⽤ JDK 1.7 的動態語⾔⽀持時,如果⼀個java.lang.invoke.MethodHandle 例項最後的解析結果是REF_getStatic 、 REF_putStatic 、 REF_invokeStatic 的⽅法控制程式碼,則需要先觸發這個⽅法控制程式碼所對應的類的初始化。
 
這 5 種情況被稱為是類的主動引⽤,注意,這⾥《虛擬機器規範》中使⽤的限定詞是 "有且僅有",那麼,除此之外的所有引⽤類都不會對類進⾏初始化,稱為被動引⽤。靜態內部類就屬於被動引⽤的情況。當 getInstance()⽅法被調⽤時,InnerClass 才在 Singleton 的運⾏時常量池⾥,把符號引⽤替換為直接引⽤,這時靜態物件 INSTANCE 也真正被建立,然後再被 getInstance()⽅法返回出去,這點同餓漢模式。那麼 INSTANCE 在建立過程中⼜是如何保證執行緒安全的呢?在《深⼊理解 JAVA 虛擬機器》中,有這麼⼀句話:虛擬機器會保證⼀個類的 <clinit>() ⽅法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化⼀個類,那麼只會有⼀個執行緒去執⾏這個類的 <clinit>() ⽅法,其他執行緒都需要阻塞等待,直到活動執行緒執⾏<clinit>() ⽅法完畢。如果在⼀個類的 <clinit>() ⽅法中有耗時很⻓的操
作,就可能造成多個程式阻塞(需要注意的是,其他執行緒雖然會被阻塞,但如果執⾏ <clinit>() ⽅法後,其他執行緒喚醒之後不會再次進⼊ <clinit>() ⽅法。同⼀個載入器下,⼀個型別只會初始化⼀次。),在實際應⽤中,這種阻塞往往是很隱蔽的。
從上⾯的分析可以看出 INSTANCE 在建立過程中是執行緒安全的,所以說靜態內部類形式的單例可保證執行緒安全,也能保證單例的唯⼀性,同時也延遲了單例的例項化。
 1 public class Singleton{
 2     private Singleton(){}//私有化構造方法
 3     public  static Singleton getInstance(){//對外提供獲取例項的公共方法
 4       return InnerClass.Instance;}
 5      private static class InnerClass{//定義靜態內部類
 6         private final static Singleton Instance=new Singleton;
 7 
 8 }
 9 
10 }

 

5.列舉:

使⽤列舉除了執行緒安全和防⽌反射調⽤構造器之外,還提供了⾃動序列化機制,防⽌反序列化的時候建立新的物件。
1 public enum Singleton{
2  instance; 
3 public void whateverMethod(){} 
4 }

 

 

相關文章