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

本文將給大家介紹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 }