Java設計模式系列之單例設計模式

我又不是架構師發表於2018-10-08

Java設計模式系列之單例設計模式

Hello,大家好,距離上次寫部落格是2018年1月26號,算了下,有8個月沒寫部落格了。這裡給大家道個歉,因為我換了工作,現就職在深圳一家公司,換了城市,加上工作上的一些事,所以一直抽不開身,2個月前不是太忙的時候,一直想著寫點什麼,可又找不到感覺了,所有就慢慢吞吞的,今天下定決心,寫點渣渣也要寫。由於博主最近準備把設計模式這一塊好好整一整,所以就從最簡單的單例模式開始,這個單例模式,說簡單也簡單,說難也難。有點生疏,大家看的時候,如果覺得寫的不好,多多包含,OK,進入正題,文章結構:

  1. 單例模式簡介
  2. 餓漢式單例模式
  3. 懶漢式執行緒安全單例模式

1. 單例模式簡介

所謂的單例模式,其實大家都知道,就是在應用程式中的某個類,無論在任何時間想拿到這個類的事例,都拿到的是唯一一個例項,那麼問題來了,什麼叫唯一的一個例項,說的再通俗點,就是拿到的那個指標(C,C++中叫指標,Java中叫引用物件)地址是唯一的。大家通過new關鍵字new幾次new出來的物件,那肯定不是唯一的了。

2. 餓漢式單例模式

上程式碼之前,先說一下,單例模式的幾個要點:

  • 建構函式私有化(上面說過了,程式碼中new出來的地址不唯一,那肯定要私有化,不能讓使用者程式碼去new)
  • 單例類提供方法或者單例物件

OK,上程式碼了,因為太簡單了,不知道怎麼再展開了。

public final class Student {
  // 注意這裡是私有化的
  private Student() {}
  // 注意這裡是私有化的
  private static final Student INSTANCE = new Student();
 // 暴露出去的方法
  public static Student getInstance() {
    return INSTANCE;
  }
}
複製程式碼

賊雞兒簡單,想拿Student物件的時候,直接Student.getInstance(); 不存在什麼執行緒安全問題,因為類內部的static變數會在類載入的時候直接建立出來。你要想整個靜態程式碼快去初始化INSTANCE變數,其實也是一樣一樣的。這裡就不寫了。其實就是利用是static變數和static程式碼快在類載入時直接載入執行原理。

3. 懶漢式執行緒安全單例模式

其實對於絕大多出場景,上面的餓漢已經絕對夠用了。比如Spring框架中的bean,預設情況下就是單例的,就是直接給你new出來,然後丟在記憶體裡,你要@Autowire的時候,直接給你。但這裡有個小問題,有些類的初始化非常耗時,比如資料庫連結,Redis連結等,這種網路IO操作。很有可能因為網路原因導致很耗時,在類被載入而他的例項還沒有被使用的時候,上面的餓漢模式顯然是不太合適的,如果這種耗時比較多的餓漢單例比較多的話,影響應用程式的啟動時間。So,我們的懶漢上場了,OK,Code:

public final class Student {

  private Student() {}
  
  private static  Student INSTANCE = null;
  
  public static Student getInstance() {
   //Mark 1 
    if(INSTANCE==null){
        INSTANCE= new Student();
    }
    return INSTANCE;
  }
}
複製程式碼

程式碼也是賊雞兒簡單,在getInstance()中判斷一下INSTANCE是不是null,如果是null(第一次呼叫)就初始化,下一次不是Null,返回舊的那個。寫到這裡,有點併發程式設計經驗的小夥伴就知道了,在Mark 1的位置,當有多個執行緒同時進入的話,會有兩個執行緒同時進入if()程式碼快,那麼就糟糕了,INSTANCE會被初始化多次。不是執行緒安全的。接下來重頭戲,我會演示幾種執行緒安全的懶漢式單例模式:

(a) synchronized 方法 保證執行緒同步

直接上程式碼:

public final class Student {

  private Student() {}
  
  private static  Student INSTANCE = null;
  // Mark 2 
  public static synchronized Student getInstance() {
   //Mark 1 
    if(INSTANCE==null){
        INSTANCE= new Student();
    }
    return INSTANCE;
  }
}
複製程式碼

其實很簡單,就是在getInstance()方法上加了synchronized關鍵字,這裡synchronized關鍵字就不展開講了,其實就是鎖住了整個getInstance()方法,保證執行緒同步,這樣當有多個執行緒進入這個方法時,會以Student.class為鎖,只有一個方法先進去,然後後面的執行緒再進去的時候,INSTANCE已經不是null了,就進入不了if程式碼塊。所以可以保證if程式碼塊在多執行緒的情況下只進入一次,也就是說Student類只被例項化一次。

(b) synchronized 程式碼塊 雙重鎖檢查(推薦)

上面的synchronized鎖住方法,其實是闊以的,但大家都知道synchronized關鍵字鎖住方法的效率還是有點小問題的,畢竟每次呼叫這個方法都加鎖,想想都很不爽 。效率不是很高,我就不多說了,所以這裡推薦使用的是synchronized關鍵字的雙重鎖檢查方式,程式碼如下:

public final class Student {

  private Student() {}

  // Mark 1 
  private static volatile  Student INSTANCE = null;

  public static Student getInstance() {
    // Mark 2 
    if(INSTANCE == null)
      synchronized (Student.class){
        // Mark 3 
        if(INSTANCE==null){
          INSTANCE= new Student();
        }
      }
    return INSTANCE;
  }
}
複製程式碼

好了,來分析下這個程式碼,先看所謂的雙重鎖檢查的Mark 2 和 Mark 3 ,如果沒有Mark 2 的話,其實和在方法上加synchronized關鍵字的效果是一樣一樣的,效率比較差,每次進這個方法後,都加一波鎖。如果沒有Mark 3 的話,大家想一想,當有兩個執行緒同時走到Mark 2 的位置,這時兩個執行緒都進入第一個If程式碼快,然後在synchronized關鍵字的時候被鎖住一個執行緒,另一個進去了,然後INSTANCE被初始化了,那麼當這個執行緒出來後,另外一個被鎖住的執行緒進去之後,如果沒有Mark 3 的話,直接執行INSTANCE= new Student(); 又例項化了一個Student,這顯然不是單例模式了。所有,Mark 2 和 Mark 3 的位置的if判斷都是不能少的,這也就是所謂的雙重鎖檢查了。這樣即能保證Student類只被例項化一次,又能保證在安全的例項化後,後續getInstance()的時候不走有鎖的程式碼了,是不是很完美。大家好好體會一下。最後值得一說的是,在Mark 1 的位置,必須保證這個INSTANCE變數是volatile 型別的,其實是為了保證執行緒可見性,保證第一個進入執行緒後的賦值操作,在後面的執行緒進入後,能夠看到,也就是說Mark 3 位置能看到。給大家一個參考文章,解釋為什麼要volatile關鍵字的。單例模式中用volatile和synchronized來滿足雙重檢查鎖機制

(c ) 靜態內部類方式
public final class Student {


  private Student() {}

  public static Student getInstance() {
  
        return StudentInner.INSTANCE;
  }

  private static class StudentInner {
  
    private static final Student INSTANCE =
        new Student();
  }
}
複製程式碼

其實就是利用了靜態內部類的載入順序問題,(靜態內部類的載入順序),只有在呼叫StudentInner.INSTANCE的時候,靜態內部類才被載入,INSTANCE變數才會被例項化,而且,類的載入肯定是執行緒安全的,不用考慮volatile和synchronized的問題。有意思不?!哈哈。

4. 結語

好了,單例模式其實就差不多了,網上也有很多相似的文章,我其實就是做了個總結,加了點自己的理解,沒什麼技術含量,後面給大家寫其他設計模式的時候可能有點技術含量,結合實際的案例,比如哪些框架裡用到了,這個單例模式,最典型的就是Spring容器裡的Bean了,要是再要舉例的話,那就是System.getSecurityManager()了,這個也是單例的。這個類是管理Java Application的許可權的,不怎麼用,我們一般都是執行容器時管理許可權,很少在程式碼裡去控制。Over,Have a good day !

相關文章