Java中的單例模式與DoubleCheck---易錯(cuò)點(diǎn)

前言

??在GoF的23種設(shè)計(jì)模式中,單例模式是比較簡單的一種。然而,有時(shí)候越是簡單的東西越容易出現(xiàn)問題。下面就單例設(shè)計(jì)模式詳細(xì)的探討一下。

??所謂單例模式,簡單來說,就是在整個(gè)應(yīng)用中保證類只有一個(gè)實(shí)例存在。這個(gè)類的實(shí)例只提供了一個(gè)全局變量,用處相當(dāng)廣泛,比如保存全局?jǐn)?shù)據(jù),實(shí)現(xiàn)全局性的操作等。

分析

??1. 最簡單的實(shí)現(xiàn) —— 餓漢式

??首先,能夠想到的最簡單的實(shí)現(xiàn)是,把類的構(gòu)造函數(shù)寫成private的,從而保證別的類不能實(shí)例化此類,然后在類中提供一個(gè)靜態(tài)的實(shí)例并能夠返回給使用者。這樣,使用者就可以通過這個(gè)引用使用到這個(gè)類的實(shí)例了。

 
public class SingletonClass { 

  private static SingletonClass instance = new SingletonClass(); 
    
  public static SingletonClass getInstance() { 
    return instance; 
  } 
    
  private SingletonClass() {  
  } 
    
}

外部使用者如果需要使用SingletonClass的實(shí)例,只能通過getInstance()方法,并且它的構(gòu)造方法是private的,這樣就保證了只能有一個(gè)對象存在。


??2. 性能優(yōu)化 —— lazy loaded 懶漢式

??上面的代碼雖然簡單,但是有一個(gè)問題——無論這個(gè)類是否被使用,都會(huì)創(chuàng)建一個(gè)instance對象。如果這個(gè)創(chuàng)建過程很耗時(shí),比如需要連接10000次jdbc實(shí)例連接或者10000多個(gè)模版實(shí)例,并且這個(gè)類還并不一定會(huì)被使用,那么這個(gè)創(chuàng)建過程就是無用的。

??為了解決這個(gè)問題,我們想到了新的解決方案:


public class SingletonClass { 

  private static SingletonClass instance = null; 
    
  public static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}

??代碼的變化有1處——把instance初始化為null,直到第一次使用的時(shí)候通過判斷是否為null來創(chuàng)建對象。

??我們來想象一下這個(gè)過程。要使用SingletonClass,調(diào)用getInstance()方法。第一次的時(shí)候發(fā)現(xiàn)instance是null,然后就新建一個(gè)對象,返回出去;第二次再使用的時(shí)候,因?yàn)檫@個(gè)instance是static的,所以已經(jīng)不是null了,因此不會(huì)再創(chuàng)建對象,直接將其返回。

這個(gè)過程就成為lazy loaded,也就是延遲加載——直到使用的時(shí)候才進(jìn)行加載。


??3. 同步

??上面的代碼很清楚,也很簡單。然而就像那句名言:“80%的錯(cuò)誤都是由20%代碼優(yōu)化引起的”。單線程下,這段代碼沒有什么問題,可是如果是多線程,麻煩就來了。我們來分析一下:
??線程1希望使用SingletonClass,調(diào)用getInstance()方法。因?yàn)槭堑谝淮握{(diào)用,1就發(fā)現(xiàn)instance是null的,于是它開始創(chuàng)建實(shí)例,就在這個(gè)時(shí)候,CPU發(fā)生時(shí)間片切換(或者被搶奪執(zhí)行),線程2開始執(zhí)行,它要使用SingletonClass,調(diào)用getInstance()方法,同樣檢測到instance是null——注意,這是在1檢測完之后切換的,也就是說1并沒有來得及創(chuàng)建對象——因此2開始創(chuàng)建。2創(chuàng)建完成后,cpu切換到1繼續(xù)執(zhí)行,因?yàn)樗呀?jīng)檢測完了,所以1不會(huì)再檢測一遍,它會(huì)直接創(chuàng)建對象。這樣,線程1和2各自擁有一個(gè)SingletonClass的對象——單例失??!

??解決的方法也很簡單,那就是加鎖:

public class SingletonClass { 

  private static SingletonClass instance = null; 
    
  public synchronized static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}


??4. 又是性能問題

??上面的代碼又是很清楚很簡單的,然而,簡單的東西往往不夠理想。理想的東西往往不夠簡單,這就是生活。這段代碼毫無疑問存在性能的問題——synchronized修飾的同步塊可是要比一般的代碼段慢上幾倍的!如果存在很多次getInstance()的調(diào)用,那性能問題就不得不考慮了!

??讓我們來分析一下,究竟是整個(gè)方法都必須加鎖,還是僅僅其中某一句加鎖就足夠了?我們?yōu)槭裁匆渔i呢?分析一下出現(xiàn)lazy loaded的那種情形的原因。原因就是檢測null的操作和創(chuàng)建對象的操作分離了。如果這兩個(gè)操作能夠原子地進(jìn)行,那么單例就已經(jīng)保證了。于是,我們開始修改代碼:

public class SingletonClass { 

  private static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if (instance == null) { 
          instance = new SingletonClass(); 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { 

  } 

}

還有問題嗎?首先判斷instance是不是為null,如果為null,加鎖初始化;如果不為null,直接返回instance。

這就是double-checked locking設(shè)計(jì)實(shí)現(xiàn)單例模式。但是還有問題。。。。。。

??5. JMM中

??在Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來屏蔽各個(gè)硬件平臺(tái)和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果。那么Java內(nèi)存模型規(guī)定了哪些東西呢,它定義了程序中變量的訪問規(guī)則,往大一點(diǎn)說是定義了程序執(zhí)行的次序。注意,為了獲得較好的執(zhí)行性能,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來提升指令執(zhí)行速度,也沒有限制編譯器對指令進(jìn)行重排序。也就是說,在java內(nèi)存模型中,也會(huì)存在緩存一致性問題和指令重排序的問題。
??下面來想一下,創(chuàng)建一個(gè)變量需要哪些步驟呢?一個(gè)是申請一塊內(nèi)存,調(diào)用構(gòu)造方法進(jìn)行初始化操作,另一個(gè)是分配一個(gè)指針指向這塊內(nèi)存。這兩個(gè)操作誰在前誰在后呢?JMM規(guī)范并沒有規(guī)定。(可能重排序)那么就存在這么一種情況,JVM是先開辟出一塊內(nèi)存,然后把指針指向這塊內(nèi)存,最后調(diào)用構(gòu)造方法進(jìn)行初始化。
??線程1開始創(chuàng)建SingletonClass的實(shí)例,此時(shí)線程2調(diào)用了getInstance()方法,首先判斷instance是否為null。按照我們上面所說的內(nèi)存模型,1已經(jīng)把instance指向了那塊內(nèi)存,只是還沒有調(diào)用構(gòu)造方法,因此2檢測到instance不為null,于是直接把instance返回了——問題出現(xiàn)了,盡管instance不為null,但它并沒有構(gòu)造完成,就像一套房子已經(jīng)給了你鑰匙,但你并不能住進(jìn)去,因?yàn)槔锩孢€是毛坯房。此時(shí),如果2在1將instance構(gòu)造完成之前就是用了這個(gè)實(shí)例,程序就會(huì)出現(xiàn)錯(cuò)誤了!

??5. 最終解決方案

??在JDK 5之后,Java使用了新的內(nèi)存模型。volatile關(guān)鍵字有了明確的語義——在JDK1.5之前,volatile是個(gè)關(guān)鍵字,但是并沒有明確的規(guī)定其用途——被volatile修飾的寫變量不能和之前的讀寫代碼調(diào)整,讀變量不能和之后的讀寫代碼調(diào)整!因此,只要我們簡單的把instance加上volatile關(guān)鍵字就可以了。

public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      synchronized (SingletonClass.class) { 
        if(instance == null) { 
          instance = new SingletonClass(); 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { 

  } 
    
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容