若是你看過設計模式,確定會知道單例模式
,實際上這是我能默寫出代碼的第一個設計模式,雖然很長一段時間我並不清楚單例具體是作什麼用的。
這裏簡單提一下單例的用處。做爲java程序員,你應該知道spring
框架,而其中最核心的IOC
,在默認狀況下注入的Bean就是單例的。有什麼好處?那些Service、Dao等只建立一次,沒必要每次都經過new方式建立,也就不用每次都開闢空間、垃圾回收等等,會省很多資源。html
那麼如何寫一個單例呢?我想不少朋友都能搞定:java
public class Singleton { private static final Singleton singletonInstance = new Singleton(); // A - 急不可待的成員變量賦值,static和final修飾 private Singleton (){} // B - 私有化的構造器,避免隨意new public static Singleton getInstance(){ // C - 暴露給外部的獲取方法 return singletonInstance; } }
Ok,擁有A、B、C三大特色(註釋部分),就構成了著名的餓漢式單例
。好處在於簡單粗暴,易於理解(只要你真正通曉final
和static
的做用)。
但有豪放派,就有婉約派。後來你們都以爲,我尚未使用這個類,你就直接把對象構建出來扔java堆裏了,是否是有點不那麼含蓄?程序員
因而你們快速迭代出懶漢式單例
。spring
class Singleton { private static Singleton singletonInstance; // A - 溫婉到只有變量聲明 private Singleton (){} // B public static Singleton getInstance(){ // C if(singletonInstance==null){ singletonInstance = new Singleton(); // D - 成員變量的建立賦值延後至此 } return singletonInstance; } }
變化發生於A、D兩步,總得來講,就是把成員變量singletonInstance
的建立和賦值延後了。基本的要求達到了,在沒調用getInstance()方法以前,對象無建立,再也不麻煩java堆大大。一切看起來都很美好,但僅限於單線程狀況下。
好,看看你們喜聞樂見的併發場景下,這種簡易的寫法會出現什麼問題——兩個線程T-1
和T-2
同時訪問getInstance()
,它們都以爲singletonInstance==null
判斷成立,分別執行了步驟D
,成功建立出singletonInstance
對象!可是,咱們通篇都在聊單例啊,T-1
和T-2
的玩法無疑很不單例!
問題分析出來了,而解決上並不複雜——讓線程同步就好。設計模式
class Singleton { private static Singleton singletonInstance; // A private Singleton (){} // B public static synchronized Singleton getInstance(){ // C - 用synchronized關鍵字修飾 if(singletonInstance==null){ singletonInstance = new Singleton(); // D } return singletonInstance; } }
惟一的變化在於步驟C
,加入了synchronized
關鍵字,讓線程同步執行此方法。如今問題解決了,無論線程T-1
仍是T-2
,在getInstance()
面前都要小朋友們排排坐——一個個執行,這樣即便是線程T-100
甚至T-500
過來也要排隊執行,哈哈哈哈哈哈……嗚嗚嗚……
既是解決方案,也是問題所在,這種方式效率太差了!併發
咱們知道,synchronized
有另外一種使用方式就是鎖代碼塊,能夠減小鎖粒度。框架
class Singleton { private static Singleton singletonInstance; // A private Singleton (){} // B public static Singleton getInstance(){ synchronized (Singleton.class){ // C - 改爲synchronized鎖代碼塊 if(singletonInstance==null){ singletonInstance = new Singleton(); } } return singletonInstance; } }
但在這個例子中,該方式看上去彷佛沒什麼提高(該方法主要邏輯只有singletonInstance = new Singleton()
一行)。好在有聰明人,研究出了Double-check
。性能
class Singleton { private static Singleton singletonInstance; // A private Singleton (){} // B public static Singleton getInstance(){ if(singletonInstance==null){ // C1 - synchronized以前,第一次判斷 synchronized (Singleton.class){ if(singletonInstance==null){ // C2 - synchronized以後,第二次判斷 singletonInstance = new Singleton(); } } } return singletonInstance; } }
我一直以爲這種方式很巧妙。C1
的判斷用於非併發環境,阻攔對象建立後的大部分訪問;C2
的判斷,解決首次建立對象時的併發問題。
很長一段時間,我以爲這就是最終方案了,世界再次變得美好,沒想到仍是圖樣圖森破(too young, too simple!)。其實不止是單例,jdk1.5以前不少問題都被一個關鍵字耽擱了——volatile
,而它相關的問題深深隱藏在Java內存模型層面,且聽我緩緩道來……優化
算了,照顧下沒耐性的開發兄弟,先給出修改方案:this
class Singleton { private static volatile Singleton singletonInstance; // A - 用volatile修飾 private Singleton (){} // B public static Singleton getInstance(){ if(singletonInstance==null){ // C1 synchronized (Singleton.class){ if(singletonInstance==null){ // C2 singletonInstance = new Singleton(); } } } return singletonInstance; } }
能夠看到,惟一的變化在於A位置
加入了volatile關鍵字
,用於解決有序性問題。(volatile
涉及的原子性和可見性這裏不做討論)
什麼是有序性?舉個「栗子」:
int x=2;//語句1 int y=0;//語句2 boolean flag=true;//語句3 x=4;//語句4 y=-1;//語句5
對於上面的代碼來講,書寫語句按順序1至5,但執行上極可能不是這樣。有多是1-4-3-2-5,或者1-3-2-5-4,其實只要保證1在4前而且2在5前,剩下的順序能夠隨意變化。這要感謝內存模型同志,它自然容許編譯器和處理器對指令進行重排序。動機是好的——能夠默默的幫你作些優化,但在併發場景下,就有好心辦壞事的嫌疑。
看下另外一個例子:
Context context = null; boolean inited = false; //線程-1: public void methodA(){ context=loadContext(); //語句1 inited=true; //語句2 } //線程-2: public void methodB(){ while(!inited){ sleep(1) //語句3 } doSomethingwithconfig(context); //語句4 }
併發場景下,極可能出現以下狀況:
線程-2
在語句3
位置無憂無慮的休眠語句2
和語句1
發生指令重排,線程-1
進入methodA()時先執行了語句2
線程-2
覺醒,執行語句4
,此時context仍是null(語句1
的context初始化還沒執行),災難產生而volatile
,是個「擋板」,能保證執行順序。爲何稱之爲「擋板」?還以以前的「栗子」說明:
int x=2;//語句1 int y=0;//語句2 volatile boolean flag=true; //語句3 - 用volatile修飾 x=4;//語句4 y=-1;//語句5
在語句3
的 boolean變量 用volatile修飾後,重排只能分別發生在一、2之間或語句四、5之間。即語句一、2不能跨過語句3,語句四、5也不能跨過語句3。
咱們還需知道,對於java的某些操做,好比++
,雖然看上去是一行代碼,但實質上這個操做自己並非原子的。以i++
爲例,該操做實際包含i
的當前值獲取,i+1
計算,以及i=
的賦值操做三兄弟。
一樣的,singletonInstance = new Singleton()
也非原子指令,包含:
若是不用volatile修飾,萬惡的指令重排可能發生在步驟2
和步驟3
之間,產生以下情況(此處有盜圖嫌疑,罪過):
以上圖的狀況,線程B
獲取到了還沒有初始化徹底的LazySingleton對象,使得在後續的使用中出現異常! 用volatile修飾singleton變量後,指令重排技能被禁用,singletonInstance = new Singleton()
只能按步驟一、二、3順序執行,問題就此解決。
值得一提的是,其實存在更好的volatile
修飾版本。
class Singleton { private static volatile Singleton singletonInstance; // A private Singleton (){} // B public static Singleton getInstance(){ Singleton tempInstance = singletonInstance; // C - 開啓了臨時變量 if(tempInstance==null){ synchronized (Singleton.class){ if(tempInstance==null){ singletonInstance = tempInstance = new Singleton(); } } } return tempInstance ; } }
這種寫法差異在於在代碼C
位置,聲明瞭變量tempInstance
臨時變量,以後的邏輯都使用tempInstance
代替singletonInstance
。爲何要這樣作?wiki上準原文是這麼說的:
Note the local variable "tempInstance ", which seems unnecessary. The effect of this is that in cases where singletonInstance is already initialized (i.e., most of the time), the volatile field is only accessed once (due to "return tempInstance ;" instead of "return singletonInstance;"), which can improve the method's overall performance by as much as 25 percent.
翻譯一下就是:singletonInstance
對象大部分時候是已完成初始化的,用tempInstance
臨時變量以後能減小volatile屬性
(singletonInstance)的訪問,這麼作大概能提高25%
的性能!
哇,一不當心寫了這麼多,並且還沒結束,留待下一篇吧。(主要是volatile
部分比較羅嗦了,這個關鍵字各位需好好看下,藉以窺探內存模型,原子性和可見性沒作分析都已經佔了這麼大的篇幅)
下一篇文章會包含靜態內部類實現單例
、final+泛型實現單例
、java9 VarHandler單例
等,敬請期待!(會有人期待嗎 ::>_<:: )