設計模式——小單例有大秘密

來源:互聯網
上載者:User

單例模式大家並不陌生,也都知道它分為什麼懶漢式、餓漢式之類的。但是你對單例模式的理解足夠透徹嗎?今天我帶大家一起來看看我眼中的單例,可能會跟你的認識有所不同。

下面是一個簡單的小執行個體:

//簡單懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static Singleton getInstance() {            if (instance == null) {         instance = new Singleton();     }        System.out.println("我是簡單懶漢式單例!");    return instance;    }}

很容易看出,上面這段代碼在多線程的情況下是不安全的,當兩個線程進入if (instance == null)時,兩個線程都判斷instance為空白,接下來就會得到兩個執行個體了。這不是我們想要的單例。


接下來我們用加鎖的方式來實現互斥,從而保證單例的實現。

//同步法懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static synchronized  Singleton getInstance() {                if (instance == null) {             instance = new Singleton();         }                System.out.println("我是同步法懶漢式單例!");        return instance;    }}

加上synchronized後確實保證了安全執行緒,但是這樣就是最好的方法嗎?很顯然它不是,因為這樣一來每次調用getInstance()方法是都會被加鎖,而我們只需要在第一次調用getInstance()的時候加鎖就可以了。這顯然影響了我們程式的效能。我們繼續尋找更好的方法。

經過分析發現,只需要保證instance = new Singleton()是線程互斥就可以保證安全執行緒,所以就有了下面這個版本:

//雙重鎖定懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static Singleton getInstance() {        if (instance == null) {             synchronized (Singleton.class) {                if (instance == null) {                     instance = new Singleton();                 }            }        }        System.out.println("我是雙重鎖定懶漢式單例!");        return instance;    }}

這次看起來既解決了安全執行緒問題,又不至於每次調用getInstance()都會加鎖導致降低效能。看起來是一個完美的解決方案,事實上是這樣的嗎?

很遺憾,事實並非我們想的那麼完美。java平台記憶體模型中有一個叫“無序寫”(out-of-order writes)的機制。正是這個機制導致了雙重檢查加鎖方法的失效。這個問題的關鍵在上面代碼上的第5行:instance = new Singleton(); 這行其實做了兩個事情:1、調用構造方法,建立了一個執行個體。2、把這個執行個體賦值給instance這個執行個體變數。可問題就是,這兩步jvm是不保證順序的。也就是說。可能在調用構造方法之前,instance已經被設定為非空了。下面我們一起來分析一下:


假設有兩個線程A、B

1、線程A進入getInstance()方法。

2、因為此時instance為空白,所以線程A進入synchronized塊。

3、線程A執行 instance = new Singleton(); 把執行個體變數instance設定成了非空。(注意,是在調用構造方法之前。)

4、線程A退出,線程B進入。

5、線程B檢查instance是否為空白,此時不為空白(第三步的時候被線程A設定成了非空)。線程B返回instance的引用。(問題出現了,這時instance的引用並不是Singleton的執行個體,因為沒有調用構造方法。) 

6、線程B退出,線程A進入。

7、線程A繼續調用構造方法,完成instance的初始化,再返回。 


難道就沒有一個好方法了嗎?好的方法肯定是有的,我們繼續探索!

//解決無序寫問題懶漢式public class Singleton {//單例執行個體變數private static Singleton instance = null;//私人化的構造方法,保證外部的類不能通過構造器來執行個體化private Singleton() {}//擷取單例對象執行個體public static Singleton getInstance() {if (instance == null) { synchronized (Singleton.class) {                  //1Singleton temp = instance;                //2if (temp == null) {synchronized (Singleton.class) {  //3 temp = new Singleton();   //4    }instance = temp;                  //5      }}}System.out.println("我是解決無序寫懶漢式單例!");return instance;}}


1、線程A進入getInstance()方法。

2、因為instance是空的 ,所以線程A進入位置//1的第一個synchronized塊。

3、線程A執行位置//2的代碼,把instance賦值給本地變數temp。instance為空白,所以temp也為空白。 

4、因為temp為空白,所以線程A進入位置//3的第二個synchronized塊。(後來想想這個鎖有點多餘)

5、線程A執行位置//4的代碼,把temp設定成非空,但還沒有調用構造方法!(“無序寫”問題) 

6、如果線程A阻塞,線程B進入getInstance()方法。

7、因為instance為空白,所以線程B試圖進入第一個synchronized塊。但由於線程A已經在裡面了。所以無法進入。線程B阻塞。

8、線程A啟用,繼續執行位置//4的代碼。調用構造方法。產生執行個體。

9、將temp的執行個體引用賦值給instance。退出兩個synchronized塊。返回執行個體。

10、線程B啟用,進入第一個synchronized塊。

11、線程B執行位置//2的代碼,把instance執行個體賦值給temp本地變數。

12、線程B判斷本地變數temp不為空白,所以跳過if塊。返回instance執行個體。


到此為止,上面的問題我們是解決了,但是我們突然發現為瞭解決安全執行緒問題,但給人的感覺就像身上纏了很多毛線.... 亂糟糟的,所以我們要精簡一下:

//餓漢式public class Singleton {//單例變數 ,static的,在類載入時進行初始化一次,保證安全執行緒 private static Singleton instance = new Singleton();    //私人化的構造方法,保證外部的類不能通過構造器來執行個體化。     private Singleton() {}//擷取單例對象執行個體     public static Singleton getInstance() {    System.out.println("我是餓漢式單例!");    return instance;}}

看到上面的代碼,瞬間覺得這個世界清靜了。不過這種方式採用的是餓漢式的方法,就是預先聲明Singleton對象,這樣帶來的一個缺點就是:如果構造的單例很大,構造完又遲遲不使用,會導致資源浪費。


到底有沒有完美的方法呢?繼續看:

//內部類實現懶漢式public class Singleton {    private static class SingletonHolder{        //單例變數          private static Singleton instance = new Singleton();    }        //私人化的構造方法,保證外部的類不能通過構造器來執行個體化。    private Singleton() {            }        //擷取單例對象執行個體    public static Singleton getInstance() {    System.out.println("我是內部類單例!");        return SingletonHolder.instance;    }}

懶漢式(避免上面的資源浪費)、安全執行緒、代碼簡單。因為java機制規定,內部類SingletonHolder只有在getInstance()方法第一次調用的時候才會被載入(實現了lazy),而且其載入過程是安全執行緒的(實現安全執行緒)。內部類載入的時候執行個體化一次instance。


簡單說一下上面提到的無序寫,這是jvm的特性,比如聲明兩個變數,String a; String b; jvm可能先載入a也可能先載入b。同理,instance = new Singleton();可能在調用Singleton的建構函式之前就把instance置成了非空。這是很多人會有疑問,說還沒有執行個體化出Singleton的一個對象,那麼instance怎麼就變成非空了呢?它的值現在是什麼呢?想瞭解這個問題就要明白instance
= new Singleton();這句話是怎麼執行的,下面用一段虛擬碼向大家解釋一下:

mem = allocate();             //為Singleton對象分配記憶體。instance = mem;               //注意現在instance是非空的,但是還沒有被初始化。ctorSingleton(instance);    //調用Singleton的建構函式,傳遞instance.

由此可見當一個線程執行到instance = mem; 時instance已為非空,如果此時另一個線程進入程式判斷instance為非空,那麼直接就跳轉到return instance;而此時Singleton的構造方法還未調用instance,現在的值為allocate();返回的記憶體對象。所以第二個線程得到的不是Singleton的一個對象,而是一個記憶體對象。


以上就是就是我對單例模式的一點小小的思考跟理解,熱烈歡迎各位大神前來指導批評。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.