📖摘要
今天分享下 —— 手寫一個線程安全的惡漢單例 的一些基本知識,歡迎關注!
網上都說單例模式是所有模式中最簡單的一種模式,巧的是我也這麼認爲。不過越簡單的東西,往往坑就隱藏的越深,這邊文章我會把我知道的幾個坑講出來。
🌂分享
什麼是單例模式
就如同他的名字一樣,‘單例’-就是隻有一個實例。也就是說一個類在全局中最多隻有一個實例存在,不能在多了,在多就不叫單例模式了。
- 小故事
- 程序員小H單身已久,每天不是對着電腦,就是抱着手機這樣來維持生活。某日,坐在電腦前,突然感覺一切都索然無味。謀生想找一個對象來一起度過人生美好的每一天。
- 於是精心打扮出門找對象,由於小H很帥,很快就找到了心儀的另一半–小K。小H的心中永遠只有小K一個人,而且發誓永遠不會在找新對象。
- 小H和小K的關係就是單例模式,在小H的全局中只有一個小K對象,且無第二個,如果有第二個的話,他們之間的關係就出問題了。哈哈
-
用在哪裏
單例模式一般用在對實例數量有嚴格要求的地方,比如數據池,線程池,緩存,session
回話等等。 -
在Java中構成的條件
- 靜態變量
- 靜態方法
- 私有構造器
單例模式的兩種形態
懶漢模式
線程不安全
public class Singleton {
private static Singleton unsingleton;
private Singleton(){}
public static Singleton getInstance(){
if(unsingleton==null){
unsingleton=new Singleton();
}
return unsingleton;
}
}
餓漢模式
線程安全
public class Singleton {
private static Singleton unsingleton=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return unsingleton;
}
}
調用
public class Test {
public static void main(String[] args) {
Singleton singleton1=Singleton.getInstance();
}
}
懶漢模式優化成線程安全
懶漢模式要變成線程安全的除了用餓漢模式之外,還有兩種方法。
加synchronized關鍵字
此方法是最簡單又有效的方法,不過對性能上會有所損失。比如兩個線程同時調用這個實例,其中一個線程要等另一個線程調用完纔可以繼續調用。而線程不安全往往發生在這個實例在第一次調用的時候發生,當實例被調用一次後,線程是安全的,所以加 synchronized
就顯得有些浪費性能。
public class Singleton {
private static Singleton unsingleton;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(unsingleton==null){
unsingleton=new Singleton();
}
return unsingleton;
}
}
用"雙重檢查加鎖"
上個方法說到,線程不安全往往發生在這個實例在第一次調用的時候發生,當實例被調用一次後,線程是安全的。那有沒有方法只有在第一次調用的時候才用 synchronized
關鍵字,而第一次後就不用 synchronized
關鍵字呢?答案是當然有的,就是用 volatile
來修飾靜態變量,保持其可見性。
public class Singleton {
private static volatile Singleton unsingleton;
private Singleton(){}
public static Singleton getInstance(){
if(unsingleton==null){
//只有當第一次訪問的時候纔會使用synchronized關鍵字
synchronized (Singleton.class){
unsingleton=new Singleton();
}
}
return unsingleton;
}
}
用"靜態內部類"
靜態內部類的優點是:外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化 INSTANCE
,故而不佔內存。即當 SingleTon
第一次被加載時,並不需要去加載 SingleTonHoler
,只有當 getInstance()
方法第一次被調用時,纔會去初始化 INSTANCE
,第一次調用 getInstance()
方法會導致虛擬機加載 SingleTonHoler
類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
public class Singleton {
private Singleton() {
}
private static Singleton instatnce;
private static class SingletonHolder {
private static Singleton instatnce = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instatnce;
}
}
那麼,靜態內部類又是如何實現線程安全的呢?首先,我們先了解下類的加載時機。
類加載時機:JAVA虛擬機在有且僅有的5種場景下會對類進行初始化。
-
遇到
new、getstatic、setstatic或者invokestatic
這4個字節碼指令時,對應的java代碼場景爲:new
一個關鍵字或者一個實例化對象時、讀取或設置一個靜態字段時(final
修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。 -
使用
java.lang.reflect
包的方法對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化。 -
當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。
-
當虛擬機啓動時,用戶需要指定一個要執行的主類(包含
main()
方法的類),虛擬機會先初始化這個類。 -
當使用
JDK 1.7
等動態語言支持時,如果一個java.lang.invoke.MethodHandle
實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic
的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
這5種情況被稱爲是類的主動引用,注意,這裏《虛擬機規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱爲被動引用。靜態內部類就屬於被動引用的行列。
我們再回頭看下 getInstance()
方法,調用的是SingleTonHoler.INSTANCE
,取的是 SingleTonHoler
裏的 INSTANCE
對象,跟上面那個 DCL
方法不同的是,getInstance()
方法並沒有多次去 new
對象,故不管多少個線程去調用getInstance()
方法,取的都是同一個 INSTANCE
對象,而不用去重新創建。當 getInstance()
方法被調用時,SingleTonHoler
纔在 SingleTon
的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象 INSTANCE
也真正被創建,然後再被 getInstance()
方法返回出去,這點同餓漢模式。那麼INSTANCE
在創建過程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機》中,有這麼一句話:
虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。如果在一個類的()方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執行()方法後,其他線程喚醒之後不會再次進入()方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。
故而,可以看出 INSTANCE
在創建過程中是線程安全的,所以說靜態內部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
那麼,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有着一個致命的缺點,就是傳參的問題,由於是靜態內部類的形式去創建單例的,故外部無法傳遞參數進去,例如 Context
這種參數,所以,我們創建單例時,可以在靜態內部類與 DCL
模式裏自己斟酌。
四種線程安全的單例模式比較
- 餓漢模式:性能好,寫法簡單,個人比較推薦用這個
- 加synchronized關鍵字:性能差,不過對懶漢模式的蓋章比較直接有效。
- volatile-雙重驗證加鎖:性能好,對Java版本有要求,要求Java5以上版本
- 靜態內部類:性能好,無需加鎖,由JVM類加載保證。
🎉最後
-
更多參考精彩博文請看這裏:《陳永佳的博客》
-
喜歡博主的小夥伴可以加個關注、點個贊哦,持續更新嘿嘿!