淺議單例模式之線程安全(轉)

[摘要]單例模式是一種常見的設計模式,在Java應用中,單例對象能保證在一個JVM中,該對象只有一個實例存在。正是由於這個特點,單例對象通常作爲程序中的存放配置信息的載體,因爲它能保證其他對象讀到一致的信息。這種方式只需訪問該單例對象即可達到統一但是在多線程環境下,但是隨着應用場景的不同,也可能帶來一些同步問題。  

    本文將探討一下在多線程環境下,使用單例對象時可能會帶來的同步問題,並給出可選的解決辦法。

 

[關鍵字] Java  設計模式  單例  線程  同步  雙重檢查鎖

 

[概念]

單例模式分類:懶漢式單例、餓漢式單例兩種。


單例模式特點:
  1、單例類只能有一個實例
  2、單例類必須自己自己創建自己的唯一實例
  3、單例類必須給所有其他對象提供這一實例

單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例

 

[問題描述]

     面試的時候,大家也許會被問到這樣一個問題:請您寫出一個單例模式(Singleton Pattern ,當然都感覺比較簡單,代碼如下:

/**

 *演示單例模式之飢餓模式

 *@authorAdministrator

 *

 */

publicclass EagerSingleton

 {

   privatestatic EagerSingleton  instance=new EagerSingleton();

     

    private EagerSingleton()

    {      

    }

    public   static  EagerSingleton  getSingleInstance()

    {      

        returninstance;

    }

}

 

   這種寫法就是所謂的飢餓模式每個對象在沒有使用之前就已經初始化了。這就可能帶來潛在的性能問題:如果這個對象很大呢?沒有使用這個對象之前,就把它加載到了內存中去是一種巨大的浪費。針對這種情況,我們可以對以上的代碼進行改進,使用一種新的設計思想——延遲加載(Lazy-load Singleton

 

/**

 *演示單例模式之懶漢模式

 *@authorAdministrator

 *

 */

publicclass LazySingleton {

 

    privatestatic LazySingleton  instance;  

    private LazySingleton()

    {

       

    }

   

    public   static  LazySingleton  getSingleInstance()

    {

        if (instance == null)

        {

                    instance = new LazySingleton();

        }     

        returninstance;

    }  

}

 

     這種寫法就是所謂的懶漢模式。它使用了延遲加載來保證對象在沒有使用之前,是不會進行初始化的。但是,通常這個時候面試官又會提問新的問題來刁難一下。他會問:這種寫法線程安全嗎?回答必然是:不安全。

測試結果:

<!--[if gte vml 1]><v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f"> <v:stroke joinstyle="miter"/> <v:formulas> <v:f eqn="if lineDrawn pixelLineWidth 0"/> <v:f eqn="sum @0 1 0"/> <v:f eqn="sum 0 0 @1"/> <v:f eqn="prod @2 1 2"/> <v:f eqn="prod @3 21600 pixelWidth"/> <v:f eqn="prod @3 21600 pixelHeight"/> <v:f eqn="sum @0 0 1"/> <v:f eqn="prod @6 1 2"/> <v:f eqn="prod @7 21600 pixelWidth"/> <v:f eqn="sum @8 21600 0"/> <v:f eqn="prod @7 21600 pixelHeight"/> <v:f eqn="sum @10 21600 0"/> </v:formulas> <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect"/> <o:lock v:ext="edit" aspectratio="t"/> </v:shapetype><v:shape id="_x0000_i1025" type="#_x0000_t75" style='width:270.75pt; height:155.25pt;mso-wrap-style:square;mso-position-horizontal-relative:page; mso-position-vertical-relative:page'> <v:imagedata src="file:///C:\Users\Kelly\AppData\Local\Temp\msohtmlclip1\01\clip_image001.png" o:title=""/> </v:shape><![endif]--><!--[if !vml]--><!--[endif]-->

 

    這是因爲在多個線程可能同時運行到判斷instancenull,於是同時進行了初始化。所以,這是面臨的問題是如何使得這個代碼線程安全?很簡單,在那個方法前面加一個SynchronizedOK

 

/**

 *演示單例模式之線程安全

 *@authorAdministrator

 *

 */

publicclass ThreadSafeSingleton

 {

    privatestatic ThreadSafeSingleton  instance;   

   

   private ThreadSafeSingleton()

    {      

    }  

  public static synchronized ThreadSafeSingleton  getSingleInstance()

    {

        if (instance == null)

        {

                    instance = new ThreadSafeSingleton();

         }    

        returninstance;

    }  

}

 

     寫到這裏,面試官可能仍然會狡猾的看了你一眼,繼續刁難到:這個寫法有沒有什麼性能問題呢?答案肯定是有的!同步的代價必然會一定程度的使程序的併發度降低。那麼有沒有什麼方法,一方面是線程安全的,有可以有很高的併發度呢?我們觀察到,線程不安全的原因其實是在初始化對象的時候,所以,可以想辦法把同步的粒度降低,只在初始化對象的時候進行同步。

 

[解決方案]

 

這裏有必要提出一種新的設計思想——雙重檢查鎖(Double-Checked Lock)。

 

/**

 *演示單例模式之雙重鎖定

 *@authorAdministrator

 *

 */

publicclass DoubleCheckedSingleton {

 

    privatestatic DoubleCheckedSingleton  instance;    

    private DoubleCheckedSingleton()

    {

 

    }

public static synchronized DoubleCheckedSingleton getSingleInstance()

    {

        //性能改進——雙重鎖定: Double-Check Locking

        if(instance==null)    //  1. 先判斷

        {

            synchronized (DoubleCheckedSingleton.class) // 2. 再同步

            {

                if (instance == null)   //3. 再判斷

                {

                    instance = new DoubleCheckedSingleton(); //4. 實例化

                }

            }

        }  

        returninstance;

    }  

}

 

     這種寫法使得只有在加載新的對象進行同步,在加載完了之後,其他線程就可以判斷當前實例對象是否爲空,如非空,並跳過鎖的的代價直接返回當前單例對象了。做到很好的併發度。

     至此,上面的寫法一方面實現了Lazy-Load,另一個方面也做到了併發度很好的線程安全,一切看上很完美。

     這是,面試官可能會對你的回答滿意的點點頭。

 

     但是,當你此時提出說,其實這種寫法還是有問題的!面試官也許會對你刮目相看!!

 

     問題在哪裏?假設線程A執行到調用上述getSingleInstance()方法,它判斷對象爲空,於是線程A執行下面初始化這個對象,但初始化是需要耗費時間的,但是這個對象的地址其實已經存在了。此時如果線程B也執行調用上述getSingleInstance()方法,它判斷不爲空,於是直接跳到最後,返回得到了這個對象。但是,這個對象還沒有被完整的初始化!得到一個沒有徹底初始化完全的對象有什麼用!!

關於這個Double-Checked Lock的討論有很多,目前公認這是一個Anti-Pattern(即:反面模式),不推薦使用!所以當這個面試官聽到你的這番答覆,他會不會被Hold不住呢?

 

那麼有沒有什麼更好的寫法呢?

有!這裏又要提出一種新的模式——Initialization on Demand  Holder.這種方法使用內部類來做到延遲加載對象,在初始化這個內部類的時候,JLS(Java Language Sepcification)會保證這個類的線程安全。這種寫法最大的巧妙在於,完全使用了Java虛擬機的機制進行同步保證,沒有一個同步的關鍵字。

/**

 *演示單例模式之完美實現

 *@authorAdministrator

 *

 */

publicclass Singleton   

{   

    privatestaticclass SingletonHolder   

    {   

        publicfinalstatic Singleton instance = new Singleton();   

    }   

  

    publicstatic Singleton getInstance()   

    {   

        return SingletonHolder.instance;   

    }   

測試結果:

單個線程

<!--[if gte vml 1]><v:shape id="_x0000_i1026" type="#_x0000_t75" style='width:282pt;height:205.5pt;mso-wrap-style:square; mso-position-horizontal-relative:page;mso-position-vertical-relative:page'> <v:imagedata src="file:///C:\Users\Kelly\AppData\Local\Temp\msohtmlclip1\01\clip_image003.png" o:title=""/> </v:shape><![endif]--><!--[if !vml]--><!--[endif]-->

 

多線程

<!--[if gte vml 1]><v:shape id="_x0000_i1027" type="#_x0000_t75" style='width:285.75pt;height:232.5pt;mso-wrap-style:square; mso-position-horizontal-relative:page;mso-position-vertical-relative:page'> <v:imagedata src="file:///C:\Users\Kelly\AppData\Local\Temp\msohtmlclip1\01\clip_image005.png" o:title=""/> </v:shape><![endif]--><!--[if !vml]--><!--[endif]-->

 

至此,單例模式以及線程安全,我們做了一個系統的比較,希望對你有所幫助!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章