天天看點

java單例模式

關于單例模式的文章,其實網上早就已經泛濫了。但一個小小的單例,裡面卻是有着許多的變化。網上的文章大多也是提到了其中的一個或幾個點,很少有比較全面且脈絡清晰的文章,于是,我便萌生了寫這篇文章的念頭。企圖把這個單例說透,說深入。但願我不會做的太差。

  首先來看一個典型的實作:

  注釋中已經有簡單的分析了。接下來分析一下關于“非線程安全”的部分。

  1、當線程A進入到第28行(#1)時,檢查instance是否為空,此時是空的。

  2、此時,線程B也進入到28行(#1)。切換到線程B執行。同樣檢查instance為空,于是往下執行29行(#2),建立了一個執行個體。接着傳回了。

  3、在切換回線程A,由于之前檢查到instance為空。是以也會執行29行(#2)建立執行個體。傳回。

  4、至此,已經有兩個執行個體被建立了,這不是我們所希望的。 

 怎麼解決線程安全問題?

  方法一:同步方法。即在getInstance()方法上加上synchronized關鍵字。這時單例變成了  

java單例模式

使用同步方法的單例

  加上synchronized後确實實作了線程的互斥通路getInstance()方法。進而保證了線程安全。但是這樣就完美了麼?我們看。其實在典型實作裡,會導緻問題的隻是當instance還沒有被執行個體化的時候,多個線程通路#1的代碼才會導緻問題。而當instance已經執行個體化完成後。每次調用getInstance(),其實都是直接傳回的。即使是多個線程通路,也不會出問題。但給方法加上synchronized後。所有getInstance()的調用都要同步了。其實我們隻是在第一次調用的時候要同步。而同步需要消耗性能。這就是問題。

  方法二:雙重檢查加鎖Double-checked locking。

  其實經過分析發現,我們隻要保證 instance = new SingletonOne(); 是線程互斥通路的就可以保證線程安全了。那把同步方法加以改造,隻用synchronized塊包裹這一句。就得到了下面的代碼:

  這個方法可行麼?分析一下發現是不行的!

  1、線程A和線程B同時進入//1的位置。這時instance是為空的。

  2、線程A進入synchronized塊,建立執行個體,線程B等待。

  3、線程A傳回,線程B繼續進入synchronized塊,建立執行個體。。。

  4、這時已經有兩個執行個體建立了。 

  為了解決這個問題。我們需要在//2的之前,再加上一次檢查instance是否被執行個體化。(雙重檢查加鎖)接下來,代碼變成了這樣:

  這樣,當線程A傳回,線程B進入synchronized塊後,會先檢查一下instance執行個體是否被建立,這時執行個體已經被線程A建立過了。是以線程B不會再建立執行個體,而是直接傳回。貌似!到此為止,這個問題已經被我們完美的解決了。遺憾的是,事實完全不是這樣!這個方法在單核和 多核的cpu下都不能保證很好的工作。導緻這個方法失敗的原因是目前java平台的記憶體模型。java平台記憶體模型中有一個叫“無序寫”(out-of-order writes)的機制。正是這個機制導緻了雙重檢查加鎖方法的失效。這個問題的關鍵在上面代碼上的第5行:instance = new SingletonThree(); 這行其實做了兩個事情:1、調用構造方法,建立了一個執行個體。2、把這個執行個體指派給instance這個執行個體變量。可問題就是,這兩步jvm是不保證順序的。也就是說。可能在調用構造方法之前,instance已經被設定為非空了。下面我們看一下出問題的過程:

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

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

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

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

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

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

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

  好吧,繼續努力,解決由“無序寫”帶來的問題。

  解釋一下執行步驟。

  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執行個體。

  好吧,問題終于解決了,線程安全了。但是我們的代碼由最初的3行代碼變成了現在的一大坨~。于是又有了下面的方法。

  方法三:預先初始化static變量。

  看到這個方法,世界又變得清淨了。由于java的機制,static的成員變量隻在類加載的時候初始化一次,且類加載是線程安全的。是以這個方法實作的單例是線程安全的。但是這個方法卻犧牲了Lazy的特性。單例類加載的時候就執行個體化了。如注釋所述:非懶加載,如果構造的單例很大,構造完又遲遲不使用,會導緻資源浪費。

  那到底有沒有完美的辦法?懶加載,線程安全,代碼簡單。

  方法四:使用内部類。

  解釋一下,因為java機制規定,内部類SingletonHolder隻有在getInstance()方法第一次調用的時候才會被加載(實作了lazy),而且其加載過程是線程安全的(實作線程安全)。内部類加載的時候執行個體化一次instance。

  最後,總結一下:

  1、如果單例對象不大,允許非懶加載,可以使用方法三。

  2、如果需要懶加載,且允許一部分性能損耗,可以使用方法一。(官方說目前高版本的synchronized已經比較快了)

  3、如果需要懶加載,且不怕麻煩,可以使用方法二。

  4、如果需要懶加載,沒有且!推薦使用方法四。

繼續閱讀