一、概述
??單例模式目的是維護(hù)系統(tǒng)中全局唯一的實例化對象离福,并對外提供全局訪問的方法溅呢。
二澡屡、示例
??單例模式分為餓漢式和懶漢式兩類,也叫快加載和懶加載藕届,二者的區(qū)別在于加載時機(jī)不同挪蹭。餓漢式指的是在程序初始化的時候就實例化唯一的對象,懶漢式指的是在程序執(zhí)行過程中休偶,真正需要這個實例對象的時候才去加載梁厉。下面我們分別看一下如何實現(xiàn):
1. 餓漢式
public class Singleton{
private static final Singleton instance = new Singleton();
// 限制默認(rèn)構(gòu)造方法的訪問權(quán)限
private Singleton(){}
// 提供獲取唯一實例對象的方法
public static Singleton getInstance(){
return instance;
}
}
??餓漢式很好理解,就是在類加載的時候就把唯一的全局對象進(jìn)行實例化,由于在JVM中词顾,每個類只會加載一次八秃,所以這個唯一實例是線程安全的,優(yōu)點不言而喻:簡單粗暴肉盹、不易出錯昔驱。
2. 懶漢式
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
??按照上面懶漢式寫法,Singleton類加載的時候上忍,只是聲明出instance骤肛,只有在調(diào)用getInstance()時才會真正地去實例化,并且在第一次執(zhí)行實例化后窍蓝,后續(xù)調(diào)用getInstance()都會直接返回唯一的實例對象腋颠。這種寫法在單線程中沒有任何問題,但是在多線程中吓笙,就肯定會出問題了淑玫,主要的問題有兩個:(1)第一個線程示例化后,其他線程不是及時可見面睛,即內(nèi)存可見性無法保證絮蒿;(2)多個線程可能同時判斷出instance為空,就會導(dǎo)致多個線程都執(zhí)行實例化代碼叁鉴。那我們可以在現(xiàn)有代碼的基礎(chǔ)上進(jìn)行改進(jìn):
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
synchronized public static Singleton getInstance(){
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
??這樣的寫法就會防止多個線程同時執(zhí)行實例化的代碼土涝,我們在getInstance方法上加一個synchronized,通過鎖機(jī)制來保證線程安全亲茅。但是回铛,每次調(diào)用getInstance()獲取單例都要獲取當(dāng)前類的鎖,就會導(dǎo)致所有線程變成了串行執(zhí)行克锣,效率肯定會受到影響茵肃。我們可以繼續(xù)改善:
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
??現(xiàn)在我們可以保證只有第一次初始化示例對象的時候才會加鎖,后續(xù)使用該示例對象都不會再次加鎖袭祟。但是這種寫法又復(fù)現(xiàn)了多次實例化的問題验残,即多個線程同時判斷到instance == null,然后依次執(zhí)行了實例化代碼巾乳。那應(yīng)該如何避免呢您没?答案是雙重檢查機(jī)制:
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();
}
}
}
return instance;
}
}
??加鎖前后,我們都對instance進(jìn)行了狀態(tài)判斷胆绊,確保只有一個線程會去實例化唯一的對象氨鹏,這么設(shè)計應(yīng)該是完美了。
??但是压状,我們忽略了一個很重要的點仆抵,就是JVM和CPU會有可能會對代碼指令重排跟继,這個重排是為了提升指令的執(zhí)行效率腮敌,而重排的原則是保證重排之后的指令執(zhí)行結(jié)果在單線程環(huán)境下和重排之前保持結(jié)果一致失球。上面的instance = new Singleton();這行代碼嫂冻,代表著實例化一個對象的過程狱意,這個過程整體上分為三步:1. 分配內(nèi)存;2. 初始化對象万牺;3. 將instance引用指向這塊內(nèi)存區(qū)域漩怎。
??如果虛擬機(jī)對這三步進(jìn)行了重排序餐济,那可能會變成1 - 3 - 2的順序趣竣,這樣就可能會出現(xiàn)異常情況了:第一個線程成功獲取到鎖摇庙,并且執(zhí)行了new Singleton(),當(dāng)實例化對象執(zhí)行到1 - 3的時候遥缕,第二個線程也調(diào)用了getInstance()方法跟匆,那么會發(fā)生什么呢?第二個線程在第一次判斷時就會得到false通砍,接著直接return instance;但這時候其實這個對象還沒有被實例化,只是指定了內(nèi)存區(qū)域烤蜕,那第二個線程使用這個對象就會出現(xiàn)問題封孙。
??為了解決上面指令重排的問題,我們加入volatile關(guān)鍵字讽营,來禁止虛擬機(jī)和CPU對關(guān)于instance的指令進(jìn)行重排序:
public class Singleton{
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
??優(yōu)化到這里虎忌,我們的懶漢加載模式終于大功告成,既保證了線程安全橱鹏,又最大限度地保證了并行處理的速度膜蠢。缺點就是邏輯判斷太多,代碼錯綜復(fù)雜莉兰,容易出錯挑围。