原創做品,能夠轉載,可是請標註出處地址:http://www.cnblogs.com/V1haoge/p/6510196.htmlhtml
所謂單例,指的就是單實例,有且僅有一個類實例,這個單例不該該由人來控制,而應該由代碼來限制,強制單例。設計模式
單例有其獨有的使用場景,通常是對於那些業務邏輯上限定不能多例只能單例的狀況,例如:相似於計數器之類的存在,通常都須要使用一個實例來進行記錄,若多例計數則會不許確。緩存
其實單例就是那些很明顯的使用場合,沒有以前學習的那些模式所使用的複雜場景,只要你須要使用單例,那你就使用單例,簡單易理解。安全
因此我認爲有關單例模式的重點不在於場景,而在於如何使用。多線程
一、常見的單例模式有兩種建立方式:所謂餓懶漢式與餓漢式學習
(1)懶漢式優化
何爲懶?顧名思義,就是不作事,這裏也是同義,懶漢式就是不在系統加載時就建立類的單例,而是在第一次使用實例的時候再建立。spa
詳見下方代碼示例:線程
1 public class LHanDanli { 2 //定義一個私有類變量來存放單例,私有的目的是指外部沒法直接獲取這個變量,而要使用提供的公共方法來獲取 3 private static LHanDanli dl = null; 4 //定義私有構造器,表示只在類內部使用,亦指單例的實例只能在單例類內部建立 5 private LHanDanli(){} 6 //定義一個公共的公開的方法來返回該類的實例,因爲是懶漢式,須要在第一次使用時生成實例,因此爲了線程安全,使用synchronized關鍵字來確保只會生成單例 7 public static synchronized LHanDanli getInstance(){ 8 if(dl == null){ 9 dl = new LHanDanli(); 10 } 11 return dl; 12 } 13 }
(2)餓漢式設計
又何爲餓?餓者,飢不擇食;但凡是有食,必急食之。此處同義:在加載類的時候就會建立類的單例,並保存在類中。
詳見下方代碼示例:
1 public class EHanDanli { 2 //此處定義類變量實例並直接實例化,在類加載的時候就完成了實例化並保存在類中 3 private static EHanDanli dl = new EHanDanli(); 4 //定義無參構造器,用於單例實例 5 private EHanDanli(){} 6 //定義公開方法,返回已建立的單例 7 public static EHanDanli getInstance(){ 8 return dl; 9 } 10 }
二、雙重加鎖機制
何爲雙重加鎖機制?
在懶漢式實現單例模式的代碼中,有使用synchronized關鍵字來同步獲取實例,保證單例的惟一性,可是上面的代碼在每一次執行時都要進行同步和判斷,無疑會拖慢速度,使用雙重加鎖機制正好能夠解決這個問題:
1 public class SLHanDanli { 2 private static volatile SLHanDanli dl = null; 3 private SLHanDanli(){} 4 public static SLHanDanli getInstance(){ 5 if(dl == null){ 6 synchronized (SLHanDanli.class) { 7 if(dl == null){ 8 dl = new SLHanDanli(); 9 } 10 } 11 } 12 return dl; 13 } 14 }
看了上面的代碼,有沒有感受很無語,雙重加鎖難道不是須要兩個synchronized進行加鎖的嗎?
......
其實否則,這裏的雙重指的的雙重判斷,而加鎖單指那個synchronized,爲何要進行雙重判斷,其實很簡單,第一重判斷,若是單例已經存在,那麼就再也不須要進行同步操做,而是直接返回這個實例,若是沒有建立,纔會進入同步塊,同步塊的目的與以前相同,目的是爲了防止有兩個調用同時進行時,致使生成多個實例,有了同步塊,每次只能有一個線程調用能訪問同步塊內容,當第一個搶到鎖的調用獲取了實例以後,這個實例就會被建立,以後的全部調用都不會進入同步塊,直接在第一重判斷就返回了單例。至於第二個判斷,我的感受有點查遺補漏的意味在內(期待高人高見)。
補充:關於鎖內部的第二重空判斷的做用,當多個線程一塊兒到達鎖位置時,進行鎖競爭,其中一個線程獲取鎖,若是是第一次進入則dl爲null,會進行單例對象的建立,完成後釋放鎖,其餘線程獲取鎖後就會被空判斷攔截,直接返回已建立的單例對象。
不論如何,使用了雙重加鎖機制後,程序的執行速度有了顯著提高,沒必要每次都同步加鎖。
其實我最在乎的是volatile的使用,volatile關鍵字的含義是:被其所修飾的變量的值不會被本地線程緩存,全部對該變量的讀寫都是直接操做共享內存來實現,從而確保多個線程能正確的處理該變量。該關鍵字可能會屏蔽掉虛擬機中的一些代碼優化,因此其運行效率可能不是很高,因此,通常狀況下,並不建議使用雙重加鎖機制,酌情使用纔是正理!
更進一步說,其實使用volatile的目的是爲了防止暴露一個未初始化的不完整單例實例,致使系統崩潰。由於建立單例實例其實須要通過如下幾步:首先分配內存空間、而後將內存空間的首地址指向引用(指針),最後調用構造器建立實例,因爲在第二步的時候這個引用(指針)就會變的非null,那麼在第三步未執行,真正的單例實例還未建立完成的時候,一個線程過來在第一個校驗中爲false,將會直接將不完整的實例返回,從而形成系統崩潰。
三、類級內部類方式
餓漢式會佔用較多的空間,由於其在類加載時就會完成實例化,而懶漢式又存在執行速率慢的狀況,雙重加鎖機制呢?又有執行效率差的毛病,有沒有一種完美的方式能夠規避這些毛病呢?
貌似有的,就是使用類級內部類結合多線程默認同步鎖,同時實現延遲加載和線程安全。
1 public class ClassInnerClassDanli { 2 public static class DanliHolder{ 3 private static ClassInnerClassDanli dl = new ClassInnerClassDanli(); 4 } 5 private ClassInnerClassDanli(){} 6 public static ClassInnerClassDanli getInstance(){ 7 return DanliHolder.dl; 8 } 9 }
如上代碼,所謂類級內部類,就是靜態內部類,這種內部類與其外部類之間並無從屬關係,加載外部類的時候,並不會同時加載其靜態內部類,只有在發生調用的時候纔會進行加載,加載的時候就會建立單例實例並返回,有效實現了懶加載(延遲加載),至於同步問題,咱們採用和餓漢式一樣的靜態初始化器的方式,藉助JVM來實現線程安全。
其實使用靜態初始化器的方式會在類加載時建立類的實例,可是咱們將實例的建立顯式放置在靜態內部類中,它會致使在外部類加載時不進行實例建立,這樣就能實現咱們的雙重目的:延遲加載和線程安全。
四、使用
在Spring中建立的Bean實例默認都是單例模式存在的。
同系列文章: