設計模式(一)之單例模式

菜_鳥成長路發表於2021-01-28

單例模式(Singleton Pattern)

單例模式:Java 中最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。

特點: 

  • 1、單例類只能有一個例項。
  • 2、單例類必須自己建立自己的唯一例項。
  • 3、單例類必須給所有其他物件提供這一例項。

優點:

  • 1、在記憶體裡只有一個例項,減少了記憶體的開銷,尤其是頻繁的建立和銷燬例項(比如管理學院首頁頁面快取)。
  • 2、避免對資源的多重佔用(比如寫檔案操作)。

缺點:沒有介面,不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來例項化。

具體實現

    (一)餓漢式

    寫法一(最常用的餓漢式寫法):

     描述:比較常用,但容易產生垃圾物件。
     優點:JVM保證單例,不需要加鎖就能保證執行緒安全,執行效率會提高。
     缺點:在類載入時就進行例項化物件,浪費記憶體。

 1 public class SingletonPatternDemo1 {
 2 
 3     private static SingletonPatternDemo1 singleton = new SingletonPatternDemo1();
 4 
 5     private SingletonPatternDemo1(){
 6 
 7     }
 8 
 9     public static SingletonPatternDemo1 getInstance(){
10         return singleton;
11     }
12 
13 }

    寫法二(通過靜態程式碼塊實現):

        寫法一和寫法二本質是相同的,都是餓漢式的實現方式,只是程式碼實現不同。

 1 public class SingletonPatternDemo2 {
 2     private static final SingletonPatternDemo2 INSTANCE;
 3 
 4     static{
 5         INSTANCE = new SingletonPatternDemo2();
 6     }
 7 
 8     private SingletonPatternDemo2(){
 9 
10     }
11 
12     public static SingletonPatternDemo2 getInstance(){
13         return INSTANCE;
14     }
15 }

 

 (二)懶漢式

             寫法一:

      描述:針對餓漢式在類載入時就進行物件例項化,浪費記憶體的缺點進行改進,使在載入類時只是申明唯一的內物件,並不進行物件例項化;而在第一次使用時進行物件的例項化。

      優點:達到了在需要時初始化的目的。

      缺點:多執行緒訪問時會存在問題,不能保證執行緒安全

 1 public class SingletonPatternDemo3 {
 2     private static SingletonPatternDemo3 INSTANCE;
 3 
 4     private SingletonPatternDemo3(){
 5 
 6     }
 7 
 8     public static SingletonPatternDemo3 getInstance(){
 9         if (INSTANCE == null){
10             INSTANCE = new SingletonPatternDemo3();
11         }
12         return INSTANCE;
13     }
14 
15     public static void main(String[] args){
16         for (int i = 0 ; i < 100 ; i++){
17             new Thread(() -> {
18                 System.out.println(SingletonPatternDemo3.getInstance().hashCode());
19             }).start();
20         }
21     }
22 }
    /*這種方式是最基本的實現方式,這種實現最大的問題就是不支援多執行緒。因為沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。
     這種方式 lazy loading 很明顯,不要求執行緒安全,在多執行緒不能正常工作。*/

   寫法二:

      描述:對於寫法一雖然達到了使用時初始化但是執行緒不安全的特點,因而催生出了寫法二使用synchronized加鎖來保證執行緒安全。

      優點:達到了在需要時初始化的目的,解決了多執行緒不安全的問題。

      缺點:使用了synchronized加鎖的方式來解決執行緒安全性問題,尤其是在class中static方法上加鎖,極大地降低了程式碼的效率。

 1 public class SingletonPatternDemo4 {
 2 
 3     private static SingletonPatternDemo4 INSTANCE;
 4 
 5     private SingletonPatternDemo4(){
 6 
 7     }
 8 
 9     public static synchronized SingletonPatternDemo4 getInstance(){
10         if (INSTANCE == null){
11             INSTANCE = new SingletonPatternDemo4();
12         }
13         return INSTANCE;
14     }
15 
16     public static void main(String[] args){
17         for (int i = 0 ; i < 100 ; i++){
18             new Thread(() -> {
19                 System.out.println(SingletonPatternDemo4.getInstance().hashCode());
20             }).start();
21         }
22     }
23 
24 }

     寫法三:

      描述:在寫法二中通過加鎖的方式解決了多執行緒安全性的問題,但同時由於加鎖位置導致程式碼效率極低;因而改變加鎖的位置是否就能滿足執行緒安全又能挽救一部分程式碼效率呢?大膽的做出假設是否不再靜態類方法上進行加鎖,改在需要保障執行緒安全的程式碼塊上加鎖,是不是就能既能保障執行緒安全也能提供程式碼效率,催生出第三種在例項化物件的程式碼塊上進行加鎖。

      優點:達到了在需要時初始化的目的,一定程度上提高了程式碼效率。

      缺點:事實證明並不能保障執行緒安全。

 1 public class SingletonPatternDemo5 {
 2     private static SingletonPatternDemo5 INSTANCE;
 3 
 4     private SingletonPatternDemo5(){
 5 
 6     }
 7 
 8     public static SingletonPatternDemo5 getInstance(){
 9         if (INSTANCE == null){
10             //妄圖通過減少同步程式碼塊的方式提高效率,然而不可行
11             synchronized(SingletonPatternDemo5.class) {
12                 INSTANCE = new SingletonPatternDemo5();
13             }
14         }
15         return INSTANCE;
16     }
17 
18     public static void main(String[] args){
19         for (int i = 0 ; i < 100 ; i++){
20             new Thread(() -> {     
21                 System.out.println(SingletonPatternDemo5.getInstance().hashCode());
22             }).start();
23         }
24     }
25 }

    通過分析程式碼:發現存在問題是因為當執行緒A和執行緒B併發執行,先是執行緒A執行到if(INSTANCE == null){}判斷並滿足條件,此時執行緒B也執行到此判斷,但是執行緒A正在申請或剛剛申請到鎖還沒有執行到例項化物件,那麼執行緒B的判斷也是true,並進入等待鎖的釋放,因而執行緒A和B都會執行例項化物件,就會產生兩個物件。所以這種寫法不能保證執行緒安全。

              也行存在這樣的疑或,為什麼不把判斷放進synchronized程式碼塊中,這樣就能保證執行緒安全。這種寫法與寫法二並沒有本質的不同。

           寫法四(雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)------以前認為最完美的方式):

      描述:通過寫法三以及寫法三的分析,在synchronized加鎖前後都進行判斷就能滿足要求,因而誕生了雙重判斷的寫法。

      優點:達到了在需要時初始化的目的,一定程度上提高了程式碼效率,執行緒安全。

      缺點:使用了synchronized加鎖就有效率的損失,而且程式碼也越來越複雜。

 1 public class SingletonPatternDemo6 {
 2 
 3     private static volatile SingletonPatternDemo6 INSTANCE;//加上volatile是因為需要解決在進行JIT設定時存在的語句重排問題
 4 
 5     private SingletonPatternDemo6(){
 6 
 7     }
 8 
 9     public static SingletonPatternDemo6 getInstance() {
10         if (INSTANCE == null) {//第一個判斷有必要嗎?有必要,當一些執行緒判斷到INSTANCE!=null時就返回了,不用都去競爭這把鎖,提高程式碼效率
11             //雙重判斷
12             synchronized (SingletonPatternDemo6.class){
13                 if (INSTANCE == null) {
14                     INSTANCE = new SingletonPatternDemo6();
15                 }
16             }
17         }
18         return INSTANCE;
19     }
20 
21     public static void main(String[] args){
22         for (int i = 0 ; i < 100 ; i++){
23             new Thread(() -> {
24                 System.out.println(SingletonPatternDemo6.getInstance().hashCode());
25             }).start();
26         }
27     }
28 
29 }

   (三)登記式 / 靜態內部類實現

      描述:這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在例項域需要延遲初始化時使用。

      優點:JVM保證單例,不存在多執行緒不安全問題,載入外部類的時候不會載入內部類,可以實現懶載入,能夠在使用時才進行例項化。

 1 public class SingletonPatternDemo7 {
 2 
 3     public SingletonPatternDemo7(){
 4 
 5     }
 6 
 7     private static class SingletonPatternDemo7Holder{
 8         private static final SingletonPatternDemo7 INSTANCE = new SingletonPatternDemo7();
 9     }
10 
11     public static SingletonPatternDemo7 getInstance() {
12         return SingletonPatternDemo7Holder.INSTANCE;
13     }
14 
15     public static void main(String[] args){
16         for (int i = 0 ; i < 100 ; i++){
17             new Thread(() -> {
18                 System.out.println(SingletonPatternDemo7.getInstance().hashCode());
19             }).start();
20         }
21     }
22 }

   (四)列舉實現

      描述:這種方式是JAVA創始人之一, 《Effective Java》的作者 Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化,但在實際中基本不用,因為這需要把class類改為enum類。

      優點:不僅保證了多執行緒的問題,還能防止反序列化。

 1 public enum SingletonPatternDemo8 {
 2 
 3     INSTANCE;
 4 
 5     public static void main(String[] args){
 6         for (int i = 0 ; i < 100 ; i++){
 7             new Thread(() -> {
 8                 System.out.println(SingletonPatternDemo8.INSTANCE.hashCode());
 9             }).start();
10         }
11     }
12 }

寫在最後:一般情況下,使用餓漢式-寫法一就可以了。只有在要明確實現 lazy loading 效果時,才會使用(三)登記式 / 靜態內部類實現的方式。如果涉及到反序列化建立物件時,可以嘗試使用(四)列舉實現的方式。如果有其他特殊的需求,可以考慮使用 懶漢式-寫法四(雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking))的方式。

相關文章