Java單例模式詳解

  • 目錄
    • 一.什么是單例?
    • 二.有幾種?
    • 三.應用場景
    • 四.注意的地方

一.什么是單例?

單例模式 保證一個類在內存中只有一個實例(對象),并提供一個訪問它的全局訪問點。

通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,并且它可以提供一個訪問該實例的方法 —— 大話設計模式--第21章

單例Boy.jpg

二.單例有幾種?

具體區分為下面幾種:

  • 餓漢式
  • 懶(飽)漢式
    • 線程不安全(單線程下操作的)
    • 線程安全
      • 方法加鎖式
      • 雙重判斷加鎖式DCL(Double-Check Locking)--- 方法加鎖式加強版
  • 靜態嵌套類式(推薦使用)
  • 枚舉式(推薦使用)
  • 使用容器單例

不和你多bb ,上代碼

b.jpg

1.餓漢式

public class Signleton{
    //對于一個final變量。  
    // 如果是基本數據類型的變量,則其數值一旦在初始化之后便不能更改;  
    // 如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。
   private final static Signleton instance = new Signleton();
   
   private Signleton(){
       
   }
   
   public static  Signleton getInstance(){
        return instance;
   }
}

解釋: 拿時間換空間,因為實例對象在類加載過程中就會被創建,在getInstance()方法中只是直接返回對象引用。因為這種創建實例對象方式比較‘急’,所以稱為餓漢式。

優點:快很準,無需關心線程安全問題。

缺點
??1.無論對象會不會被使用,在類加載的時候就創建對象了,這樣會降低內存的使用率。
?? 2.如果在一個大環境下使用了過多的餓漢單例,則會生產出過多的實例對象,無論你是否要使用他們

餓.jpg

2.懶(飽)漢式

(1)線程不安全(單線程下操作的)

public class Signleton{
  private static Signleton instance = null;
  
  private Signleton(){
  
  }
  
  public static Signleton getInstance(){
     if(instance==null){
        instance = new Signleton();
     }
     return instance;
  }
}

解釋: Singleton的靜態屬性instance中,只有instance為null的時候才創建一個實例,構造函數私有,確保每次都只創建一個,避免重復創建。之所以被稱為“懶漢”,因為它很懶,不急著生產實例,在需要的時候才會生產。

優點:延遲加載

缺點:只在單線程的情況下正常運行,在多線程的情況下,就會出問題。例如:當兩個線程同時運行到判斷instance是否為空的if語句,并且instance確實沒有創建好時,那么兩個線程都會創建一個實例。

(2)線程安全

方法加鎖式
public class Signleton{
  private static Signleton instance = null;
  
  private Signleton(){
  
  }
  //給方法加鎖 若有ABCD線程使用 A線程先進入 BCD線程都需要等待A線程執行完畢釋放鎖才能獲得鎖執行該方法
  //這樣效率較低 
  public syschronized static Signleton getInstance(){
     if(instance==null){
        instance = new Signleton();
     }
     return instance;
  }
}

解釋:給方法添加[synchronized],使之成為同步函數。兩個線程同時想創建實例,由于在一個時刻只有一個線程能得到同步鎖,當第一個線程加上鎖以后,第二個線程只能等待。第一個線程發現實例沒有創建,創建之。第一個線程釋放同步鎖,第二個線程才可以加上同步鎖,執行下面的代碼。由于第一個線程已經創建了實例,所以第二個線程不需要創建實例。保證在多線程的環境下也只有一個實例。

優點: 保證了線程安全。延時加載,用的時候才會生產對象。

缺點: 需要保證同步,付出效率的代價。加鎖是很耗時的。。。

雙重判斷加鎖式DCL
public class Signleton {
  private static Signleton instance = null;
  
  private Signleton(){
  
  }

  //假設有ABCD 個線程 使用這個方法
  public static Signleton getInstance(){
  //BCD都進入了這個方法
    if(instance==null){
      //而A線程已經給第二個的判斷加鎖了 
      syschronized(Signleton.class){
         //這時A掛起,對象instance還沒創建 ,故BCD都進入了第一個判斷里面,并排隊等待A釋放鎖
         //A喚醒繼續執行并創建了instance對象,執行完畢釋放鎖。
         //此時到B線程進入到第二個判斷并加鎖,但由于B進入第二個判斷時instance 不為null了  故需要再判斷多一次  不然會再創建一次實例
          if(instance==null){
             instance = new Signleton();
          }
       
      } 
    }
    return instance;
   }
}

解釋:方法加鎖式的優化。只有當instance為null時,需要獲取同步鎖,創建一次實例。當實例被創建,則無需試圖加鎖。。

優點: 保證了線程安全。延時加載,用的時候才會生產對象。進行雙重判斷,當已經創建過實例對象后就無需加鎖,提高效率。

缺點: 編寫復雜、難記憶。雖然是優化加鎖式,但加鎖始終會耗時。

3.靜態嵌套類式(推薦使用)
public class Signleton{
   private Signleton{
   }
  
   //靜態嵌套類  這里給個鏈接 區分靜態嵌套類和內部類[靜態嵌套類和內部類](http://blog.csdn.net/iispring/article/details/46490319)
   private static class  SignletonHolder{
      public static final Signleton instance = new Signleton();
  }
  
  public static Signleton getInstance(){
   return SignletonHolder.instance;
ins't
  }
  
}

解釋: 定義一個私有的靜態嵌套類,在第一次用這個嵌套類時,會創建一個實例。而類型為SingletonHolder的類,只有在Singleton.getInstance()中調用,由于私有的屬性,他人無法使用SingleHolder,不調用Singleton.getInstance()就不會創建實例。

優點: 保證了線程安全。無需加鎖,延遲加載,第一次調用Singleton.getInstance才創建實例。推薦使用。

缺點: 無。

4.枚舉式(推薦使用)

為了方便理解枚舉式 這邊簡單介紹一下枚舉(在jdk1.5后)

//枚舉Type
public enum Type{
   A,B,C;
  private String type;
  Type(type){
     this.type = type;
 }
 public String getType(){
    return type;
  }
}
//可認為等于下面的
public class Type{
  public static final Type A=new Type(A);
  public static final Type B=new Type(B);
  public static final Type C=new Type(C);
ins't
}

所以Type.A.getType()為A.
推薦去了解一下
Java學習整理系列之Java枚舉類型的使用
Java學習整理系列之Java枚舉類型的原理

好了 開始介紹枚舉式了 看代碼

public class Signleton{

public static Signleton getInstance(){
   return SignletonEnum.INSTANCE.getInstance();
}

public enum SignletonEnum{
   INSTANCE;
   
   private Signleton instance;
   
   //由于JVM只會初始化一次枚舉實例,所以instance無需加static 
   private SignletonEnum(){
        instance = new Signleton();
   }
   
   public getInstance(){
       return instance;   
   }
}

}

解釋: 定義內部的枚舉,由于類加載時JVM只會初始化一次枚舉實例,所以在構造函數中創建Signgleton對象并保證了這個對象實例唯一。
通過調用枚舉INSTANCE方法getInstance (SignletonEnum.INSTANCE.getInstance())獲取實例對象。

優點: 枚舉提供了序列化機制--例如在我們要通過網絡傳輸一個數據庫連接的句柄,會提供很多幫助。

單元素的枚舉類型已經成為實現Singleton的最佳方法。Effective Java

5.使用容器單例
public class Singleton { 
   //用Map保存該單例
    private static Map<String, Object> objMap = new HashMap<>(); 

    private Singleton() { 

    } 

    public static void putObject(String key, String instance){ 
        if(!objMap.containsKey(key)){ 
            objMap.put(key, instance); 
        } 
    } 

    public static Object getObject(String key){ 
        return objMap.get(key); 
    } 
}

解釋: 在程序開始的時候將單例類型注入到一個容器之中 ,在使用的時候再根據 key 值獲取對應的實例,這種方式可以使我們很方便的管理很多單例對象,也對用戶隱藏了具體實現類,降低了耦合度。

缺點: 會造成內存泄漏。(所以我們一般在生命周期銷毀的時候也要去銷毀它) 。

三.應用場景

一般創建一個對象需要消耗過多的資源,如:訪問I0和數據庫等資源或者有很多個地方都用到了這個實例。

四.注意的地方

雙重判斷加鎖式DCL :這種寫法也并不是保證完全100%的可靠,由于 java 編譯器允許執行無序,并且 jdk1.5之前的jvm ( java 內存模型)中的 Cache,寄存器到主內存的回寫順序規定,第二個和第三個執行是無法保證按順序執行的,也就是說有可能1-2-3也有可能是1-3-2; 這時假如有 A 和 B 兩條線程, A線程執行到3的步驟,但是未執行2,這時候 B 線程來了搶了權限,直接取走 instance 這時候就有可能報錯。

簡單總結就是說jdk1.5之前會造成兩個問題:

1、線程間共享變量不可見性;

2、無序性(執行順序無法保證);

當然這個bug已經修復了,SUN官方調整了JVM,具體了Volatile關鍵字,因此在jdk1.5之前只需要寫成這樣既可, private Volatitle static Singleton instance; 這樣就可以保證每次都是從主內存中取,當然這樣寫或多或少的回影響性能,但是為了安全起見,這點性能犧牲還是值得。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1 場景問題# 1.1 讀取配置文件的內容## 考慮這樣一個應用,讀取配置文件的內容。 很多應用項目,都有與應用相...
    七寸知架構閱讀 6,888評論 12 68
  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個類僅有一個...
    tandeneck閱讀 2,540評論 1 8
  • 單例模式(SingletonPattern)一般被認為是最簡單、最易理解的設計模式,也因為它的簡潔易懂,是項目中最...
    成熱了閱讀 4,298評論 4 34
  • 單例模式(Singleton Pattern)是眾多設計模式中較為簡單的一個,同時它也是面試時經常被提及的問題,如...
    廖少少閱讀 601評論 0 1
  • 》昨晚喝了滿滿一湖的西湖水《 雨前悶熱的空氣 穹罩大地 如一空明不透的玻璃鏡 要怎樣 打破窒息般的結界 出門撒歡釋...
    崖邊草閱讀 250評論 0 0