1. 程式人生 > >第六章:單利模式,懶漢式,餓漢式以及靜態內部類,雙重檢查

第六章:單利模式,懶漢式,餓漢式以及靜態內部類,雙重檢查

文章轉自:https://blog.csdn.net/u012453843/article/details/73743997

單例模式,最常見的就是飢餓模式和懶漢模式,一個直接例項化物件,一個在呼叫方法時進行例項化物件。

       大家見的最多的莫過於下面這種單例模式了,這種模式是懶漢模式,就是說只有你呼叫getInstance方法的時候,它才會建立例項。但是這種方式有個非常致命的問題就是在多執行緒的情況下不能正常工作。

  1. public class Singleton {    
  2.     private static Singleton instance;    
  3.     private Singleton (){}    
  4.     public static Singleton getInstance() {    
  5.        if (instance == null) {    
  6.           instance = new Singleton();    
  7.        }    
  8.        return instance;    
  9.     }    
  10. }   
        懶漢模式要想執行緒安全,大家第一想到的便是下面這種方式,就是在getInstance方法加上synchronized關鍵字,但是這種方式也有致命的缺點,那就是併發率太低。
  1. public class Singleton {    
  2.     private static Singleton instance;    
  3.     private Singleton (){}    
  4.     public static synchronized Singleton getInstance() {    
  5.        if (instance == null) {    
  6.           instance = new Singleton();    
  7.        }    
  8.        return instance;    
  9.     }    
  10. }  
        上面是懶漢模式,下面我們再看下餓漢模式,如下所示。餓漢模式是典型的空間換取時間,當類裝載的時候就會建立類的例項,不管你用不用,先創建出來,然後每次呼叫的時候就不需要再判斷了,節省了執行時間。但如果一直沒有人呼叫,這種浪費的空間就不值得,特別是在空間不足的情況下。
  1. public class Singleton {    
  2.     private static Singleton instance = new Singleton();    
  3.     private Singleton (){}    
  4.     public static Singleton getInstance() {    
  5.        return instance;    
  6.     }    
  7. }   
         在多執行緒模式中,考慮到效能和執行緒安全問題,我們一般選擇下面兩種比較經典的單例模式,在效能提高的同時,又保證了執行緒安全。
第一種方式:靜態內部類
         這種方式是最好的單例模式,而且還是執行緒安全的,為何這麼說呢,靜態內部類Singleton在初始化過程中是不會被載入的,只有當用戶呼叫共用的getInstance方法時才會載入內部類Singleton並且例項化Singleton例項,也就是說,靜態內部類這種方式也屬於懶漢模式,只是實現方式不一樣而已。之所以是執行緒安全的,是因為Singleton是靜態的,靜態內部類只會被例項化一次,也就是說不管有多少執行緒,大家拿到的是同一個例項,不會再去進行多次例項化,從而達到了執行緒安全的目的。由於沒有加鎖,所以併發性特別高,執行緒還安全,所以大家以後碰到單例模式,用靜態內部類最為合適。如下圖所示。

          程式碼如下:

  1. package com.internet.singleton;  
  2. public class InnerSingleton {  
  3.     private static class Singleton{  
  4.         private static InnerSingleton single = new InnerSingleton();  
  5.     }  
  6.     public static InnerSingleton getInstance(){  
  7.         return Singleton.single;  
  8.     }  
  9. }  
第二種方式:雙重檢查
           首先,我說一下這種設計的初衷是什麼,我們看到了getDs()方法中添加了類鎖(synchronized (DubbleSingleton.class)),這種鎖直接把整個類都鎖住了,其它執行緒訪問這個類的任何方法都要排隊等候,設計者目的是當第一個執行緒訪問getDs()方法時,它把整個類鎖住,然後它把這個類例項化,然後後面的執行緒進入getDs()方法後一判斷髮現ds已經不是null了,於是便把第一個執行緒例項化好的例項返回。這樣後續的執行緒無論併發有多少都沒有問題,也不用再排隊,直接拿取例項化好的例項即可。既然這樣,為何在synchronized (DubbleSingleton.class)當中再判斷一次ds是否為null呢?這個其實也好理解,synchronized上面不是模擬了一下初始化物件的準備時間嗎?這個模擬是很有必要的,當第一個執行緒進入getDs()方法後,判斷了一下df是否為null,發現是null,然後它就進入初始化物件的準備工作中去了,這個過程可能需要幾秒鐘,注意這時第一個執行緒還沒有執行到synchronized這一行程式碼,也就是說還沒有加上類鎖,在這個過程當中假如又有多個執行緒要呼叫getDs()方法,它們便可以同時訪問這個方法,這些執行緒判斷ds依然是null,因此會進入到if判斷裡面,這些執行緒也進入到初始化物件的準備過程,等到第一個執行緒初始化物件準備完畢之後,它進入到synchronized這塊程式碼處,給整個類加上了鎖,這時後續再有執行緒的話,就要排隊等候呼叫getDs()了。等第一個執行緒執行完例項化程式碼之後,剛才那些趁第一個執行緒沒有鎖住類的間隙偷偷摸進來的執行緒便也都進入到synchronized程式碼處,這些執行緒會依次鎖住類進入到synchronized程式碼塊中,這時如果程式碼塊中不加一層if判斷的話,就會再例項化一次DubbleSingleton類並返回,有幾個這樣偷溜進來的執行緒便會例項化幾次Dubblesingleton例項。除了這些偷溜進來的執行緒之外,再來訪問的執行緒由於在類外等待第一個執行緒執行完之後才有機會進入到方法體中,這時ds早已經例項化過了,因此第一個if判斷便不成立,於是直接把當前Dubblesingleton例項返回。也就是說,後面的執行緒便可以高併發了,不用受鎖的限制了。而如果在synchronized鎖內再加一層判斷的話,由於第二個執行緒進入鎖內時,第一個執行緒肯定已經執行完了(synchronized這時扮演的是類鎖),因此這時ds肯定不是null了,第二個執行緒一判斷,發現ds不為null了,便直接把第一個執行緒例項化好的例項返回了,同理,其它執行緒也把第一個執行緒例項化好的例項返回。從而保證了執行緒安全。
         雙重檢查與懶漢加鎖模式最大的區別在於,雙重檢查加鎖只是一瞬間的事兒,後續無論有多少個執行緒都可以自由訪問,沒有執行緒安全問題。而懶漢加鎖模式由於每個執行緒都要排隊訪問getInstance方法,因此效率太低。這就是他們之間的區別。雙重檢查的程式碼類如下圖所示。

          程式碼如下:

  1. package com.internet.singleton;  
  2. public class DubbleSingleton {  
  3.     private static DubbleSingleton ds;  
  4.     public static DubbleSingleton getDs(){  
  5.         if(ds == null){  
  6.             try {  
  7.                 //模擬初始化物件的準備時間...  
  8.                 Thread.sleep(3000);  
  9.             } catch (Exception e) {  
  10.                 e.printStackTrace();  
  11.             }  
  12.             synchronized (DubbleSingleton.class) {  
  13.                 if(ds == null){  
  14.                     ds = new DubbleSingleton();  
  15.                 }  
  16.             }  
  17.         }  
  18.         return ds;  
  19.     }  
  20.     public static void main(String[] args){  
  21.         Thread t1 = new Thread(new Runnable() {  
  22.             @Override  
  23.             public void run() {  
  24.                 System.out.println(DubbleSingleton.getDs().hashCode());  
  25.             }  
  26.         },"t1");  
  27.         Thread t2 = new Thread(new Runnable() {  
  28.             @Override  
  29.             public void run() {  
  30.                 System.out.println(DubbleSingleton.getDs().hashCode());  
  31.             }  
  32.         },"t2");  
  33.                 Thread t3 = new Thread(new Runnable() {  
  34.             @Override  
  35.             public void run() {  
  36.                 System.out.println(DubbleSingleton.getDs().hashCode());  
  37.             }  
  38.         },"t3");  
  39.                 t1.start();  
  40.                 t2.start();  
  41.                 t3.start();  
  42.     }  
  43. }  
         執行上面雙重檢查程式碼,結果如下圖所示,可見,例項是一樣的。

         如果把synchronized程式碼塊中的if判斷去掉,如下圖所示。

          再執行main方法,結果如下圖所示,發現三次結果都不一樣,說明有執行緒安全問題。


           綜合以上所有情況,可以知道,用靜態內部類是最好的單例模式。