簡介
什么是單例灯谣?為什么需要單例缆八?
單例模式的目的是設(shè)計出一個類曲掰,能提供全局唯一的對象疾捍。
舉個例子,程序中需要一個類用做管理配置項栏妖。這樣一個類顯然希望是全局只有一個乱豆,在任何地方都能獲取和使用,任何地方使用的對象也都是同一個底哥。這種情況就需要使用單例模式咙鞍!
簡單實現(xiàn)
舉例來說,我們要寫一個Singleton
類的單例趾徽,首先我們先寫一個類:
public class Singleton{
}
要保證類是單例的,那么就不能隨意通過new
操作符來隨意創(chuàng)建實例翰守。否則孵奶,每個new
出來的對象都不是同一個對象,哪有單例可言蜡峰。為了禁止new
操作符創(chuàng)建對象了袁,需要顯式地將構(gòu)造函數(shù)聲明為private
:
public class Singleton{
private Singleton(){}
}
既然不能隨意創(chuàng)建,那么總得有一個方法能夠返回單例對象吧湿颅,我們命名為getInstance
:
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
//return a unique Singleton instance;
}
}
重點:
getInstance
必須是static
方法载绿,因為只有static
方法才可以直接通過Class調(diào)用getInstance
方法提供的必須是唯一實例
那么問題來了,getInstance
方法如何能提供唯一實例呢油航?
第一種簡單的方法如下:
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return this.instance;
}
}
這種模式叫做餓漢模式
崭庸,即一開始就創(chuàng)建一個對象,每次通過getInstance
方法返回時谊囚,就返回這個對象怕享。很明顯,這個一定是單例的镰踏,全局只有一個Singeton
對象函筋。
重點:
instance
必須是static的。因為static
方法中使用的只能是static
對象奠伪。(這是因為static方法是可以通過class直接調(diào)用的跌帐,而非static成員必須有instance的時候才能使用。)
餓漢模式是如何保證線程安全的绊率?
JVM保證static成員只會被初始化一次谨敛。
缺點
“餓漢模式”有個缺點:
- 只要類被加載,對象就會被創(chuàng)建即舌,不管有沒有調(diào)用
getInstance
方法佣盒。如果單例對象是個大對象,沒有用到但又占著內(nèi)存空間顽聂,是比較浪費的肥惭。 - 如果單例對象的創(chuàng)建依賴于某些配置項盯仪,必須先配置,再創(chuàng)建對象蜜葱,那么餓汗模式是沒法使用的全景。
因此,衍生出另一種對應(yīng)的模式“懶漢模式”牵囤。
“懶漢模式”的特點是只有在使用的時候才創(chuàng)建對象爸黄。為了實現(xiàn)這種效果,很容易想到下面這種寫法:
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
關(guān)鍵點是揭鳞,先判斷instance
對象有沒有已經(jīng)被創(chuàng)建過炕贵,如果等于null,說明沒有被創(chuàng)建過野崇,則創(chuàng)建一個称开,否則直接返回已有對象。
是不是感覺這樣寫沒毛才依妗鳖轰?No!No!No!這樣寫毛病很大!因為不是線程安全的扶镀。當(dāng)多個線程幾乎同時檢測if(instance == null)
時蕴侣,都發(fā)現(xiàn)instance為null,這時都繼續(xù)往下走臭觉,從而創(chuàng)建了多個對象昆雀。如下圖所示:
如何寫出線程安全,并且性能良好的單例胧谈,一直是一個常見的面試題忆肾。
可能很多人可以很快給出以下方案:
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
這個方案是通過添加synchronized
關(guān)鍵字來同步方法。
重點:
- 使用
synchronized
來實現(xiàn)線程安全菱肖。在不考慮性能的情況下客冈,絕對簡單、有效稳强。
缺點:
這個方案很顯然能夠?qū)崿F(xiàn)線程安全场仲,但是性能堪憂。每次獲取單例對象都要進行同步退疫,有必要嗎渠缕?試想,當(dāng)一個對象創(chuàng)建以后褒繁,以后的所有操作都只是讀亦鳞,讀也同步的話,很顯然是影響效率的。
getInstance方法可以這樣寫嗎燕差?
public static Singleton getInstance(){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
可以遭笋!synchronized(Singleton.class)
是個類鎖,在static
方法上加synchronized
關(guān)鍵字同樣是類鎖徒探,兩者是一樣的瓦呼。
引申
synchronized關(guān)鍵字 On the way...
簡單的加synchronized
關(guān)鍵字的方案的問題主要在于鎖的粒度太大。只是簡單的像下面一樣減小鎖的粒度也是不行的:
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
這個方案又重新引入了線程安全問題测暗,synchronized
關(guān)鍵字只是使并發(fā)創(chuàng)建對象編程了順序進行央串。這個方案直接pass。那么如何既能減小鎖的粒度碗啄,又能保證線程安全呢质和?
DCL
上面的方案減小的鎖的粒度,單不是線程安全稚字。我們可以在上面方案的基礎(chǔ)上繼續(xù)修改:
public class Singleton{
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這個方案就是Double Check Lock(簡稱DCL)
侦另。這個方案在上面方案的基礎(chǔ)上做了改進:synchronized
同步塊里面能夠保證只創(chuàng)建一個對象。但是通過在synchronized
的外面增加一層判斷尉共,就可以在對象一經(jīng)創(chuàng)建以后,不再進入synchronized
同步塊弃锐。這種方案不僅減小了鎖的粒度袄友,保證了線程安全,性能方面也得到了大幅提升霹菊。
現(xiàn)在的方案是不是已經(jīng)很完美了剧蚣?
并!不旋廷!是鸠按!
為什么呢?
- 一個單例可以通過短的更多的代碼實現(xiàn)饶碘。
- DCL在極小的概率下目尖,創(chuàng)建的對象不可用!會報錯扎运!
我們首先講第二個問題瑟曲,DCL為何產(chǎn)生的對象有可能不可用。這是instance = new Singleton()
不是原子操作豪治,而是可以大致分為以下三個步驟:
- 給
Singleton
對象分配內(nèi)存- 初始化
Singleton
對象- 將創(chuàng)建的對象引用賦值給
instance
單線程情況下洞拨,instance = new Singleton
會嚴(yán)格按照上述1,2,3步驟逐次執(zhí)行,并不會出錯负拟。但是在多線程情況下烦衣,由于在優(yōu)化時編譯器和CPU指令重排的存在,上述步驟執(zhí)行的次序有可能為1,3,2,這樣花吟,有可能出現(xiàn)如下的情況:
我們期望的順序是1-2-3秸歧,但是由于指令重排,實際的順序可能是1-3-2示辈,這種情況下寥茫,線程1創(chuàng)建了一個單例對象,雖然instance
已經(jīng)賦值了矾麻,但是對象還沒有初始化纱耻,線程2在第一次check instance
是否為null的時候,得到對象已經(jīng)創(chuàng)建的錯誤信息险耀,就直接使用弄喘,顯然會出錯。這種情況出現(xiàn)概率極低甩牺,系統(tǒng)低負(fù)載的時候很難出現(xiàn)蘑志,但是高負(fù)載時,一旦出現(xiàn)問題贬派,就很難調(diào)查急但!
那么,有什么解決方案呢搞乏?可以使用volatile
關(guān)鍵字波桩。volatile
關(guān)鍵字修飾了instance
后,有兩個作用:
- 禁止對
instance
的操作重排指令- 并能保證多線程之間
instance
對象的及時可見性请敦。
如下:
public class Singleton{
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
重點
- 使用volatile關(guān)鍵字禁止指令重排镐躲。
- 這個事情僅在Java 1.5版后有用,1.5版之前用這個變量也有問題侍筛,因為老版本的Java的內(nèi)存模型是有缺陷的萤皂。
引申
詳解指令重排 On the way...
詳解volatile On the way...
還記不記得上面提到,單例可以有更短的代碼匣椰?對裆熙!使用靜態(tài)內(nèi)部類!
靜態(tài)內(nèi)部類
使用靜態(tài)內(nèi)部類實現(xiàn)單例模式的通用寫法如下:
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return InnerClass.getInstance();
}
private static class InnerClass{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
}
重點:
- 在靜態(tài)內(nèi)部類中創(chuàng)建單例對象窝爪。JVM保證static對象的創(chuàng)建是線程安全的弛车。
- JVM還保證,一個類的任何static方法沒被調(diào)用的情況下蒲每,所有的static成員都不會被加載和初始化纷跛。這樣能保證單例對象只有在用的時候才會創(chuàng)建。
- 依舊是懶漢模式邀杏,只有需要時才創(chuàng)建
內(nèi)部類可以是public的嗎贫奠?
不可以唬血!我們期望只有通過調(diào)用
getInstance
方法才能獲得單例對象,如果內(nèi)部類是public的唤崭,就破壞了封裝性拷恨。
instance成員可以是public的嗎?
不建議谢肾。成員對象用public修飾腕侄,同樣破壞了類的封裝性。不過非要這么寫芦疏,也沒辦法冕杠。
其實,還有更短的代碼K彳睢7衷ぁ!
枚舉
public enum Singleton{
INSTANCE;
}
重點
- 默認(rèn)枚舉實例的創(chuàng)建是線程安全的薪捍,所以不需要擔(dān)心線程安全的問題笼痹。
- 《Effective Java》中推薦的模式
總結(jié)
單例模式是一個很常用的設(shè)計模式,也是一個在面試中常被問到的設(shè)計模式酪穿,更是一個很容易答錯的設(shè)計模式凳干。
單例模式雖然看起來簡單,但是設(shè)計的Java基礎(chǔ)知識非常多被济,如static修飾符纺座、synchronized修飾符、volatile修飾符溉潭、enum等。能正確地寫出說容易也行少欺,說難也行喳瓣。這個難易就在于個人的理解程度!很多人面試時遇到這個問題赞别,經(jīng)常丟三落四畏陕,少些一些關(guān)鍵修飾符,或者不清楚怎么寫仿滔。關(guān)鍵的點在于惠毁,不要死記硬背,一定要理解崎页!
同時鞠绰,更凸顯出java基礎(chǔ)知識的重要性!需不需要static飒焦,可不可以public蜈膨,這些都是有java基礎(chǔ)語法做為論據(jù)的屿笼。深刻理解了java語法,才能以不變應(yīng)萬變翁巍,才不怕忘記驴一!
如有問題,請留言灶壶,我會及時回復(fù)肝断,一起討論問題。