一、單例模式簡介
單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個例項。即一個類只有一個物件例項。在java程式碼中,通常new關鍵字創造出來的物件,對系統的開銷一般都挺大的。所以在某些情況下,單例的實現也是應對系統優化的一種解決辦法。
二、單例模式的實現
常見的單例有這幾種實現
- 餓漢式
- 飽漢式
- 雙重校驗
- 靜態內部類
1、餓漢式
先來介紹餓漢式,餓漢式,顧名思義,就是一進入這個類,該類的例項就被初始化完成了。接下來來看下程式碼。
public class Demo {
private static Demo h = new Demo();
private Demo(){
}
public static Demo getInstance(){
return h;
}
}
複製程式碼
程式碼也和簡單,就是直接構造一個私有的構造器,然後在建立一個成員變數,順便例項化該類,在呼叫該類的getInstance方法,當然前面也說過是一進入這個類,
該類的例項就被建立完成,所以也可以利用類的載入順序來編寫這個程式碼。比如 A類是B類的子類,在初始化A類的例項的時候,會先去父類B中去,看看有沒有靜態塊和靜態成員變數(靜態方法只有被呼叫時才會載入,且只會被載入一次),若有則先去載入B類的靜態塊和靜態成員變數,再載入A類的。之後會去呼叫B的構造器,最後才會呼叫本類對應的構造器。
所以我們可以在靜態程式碼塊中例項化。如下程式碼
public class Demo {
private static Demo h = null;
private Demo(){
}
static{
h = new Demo();
}
public static Demo getInstance(){
return h;
}
}
複製程式碼
該種實現的單例是執行緒安全的。當然由於它會提前初始化,所以會提前佔用一些系統資源。
2、飽漢式
飽漢式的構造例項的時候與餓汗式相反,它只有在第一次需要的時候才會去構造例項。具體實現程式碼如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
//1
h = new Demo();
//2
}
return h;
}
}
複製程式碼
飽漢式最常見的的編寫方式就是上述程式碼,對於剛瞭解單例模式的人來說,飽漢式就寫完了,不過在單執行緒環境也確實可以說是寫完了,A執行緒在獲取例項,第一次獲取時,看見為null,進行初始化,第二次,不是null,直接返回。這也是一種很理想的流程。但是值得注意的是,在多執行緒下,它就值得推敲了。比如看下面例子
- 執行緒A:嘿嘿!我已經走到了2,這初始化的好處我就要獨佔了,想想都雞凍,我要去初始化Demo類的例項了,啦啦啦。
- 執行緒B:哈哈!執行緒A那個SD,我都走到了1,它竟然還沒發現我,還想獨佔Demo的例項化,沒門!!
旁白:執行緒A還完成了對該類例項的初始化,執行緒B也進入了對該例項的構造中,
因此,執行緒A和執行緒B都同時初始化了該例項,這也不滿足單例的條件。
於是有人很自然的想到,加鎖。如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public synchronized static Demo getInstance(){
if(h == null){
h = new Demo();
}
return h;
}
}
複製程式碼
這樣確實可以防止多執行緒環境造成多個例項。但的缺點是每一次獲取都去加鎖,會對效能有一定的損失。所以有了雙重校驗鎖。
3、雙重校驗
雙重校驗,就是在獲取單例的時候,對加鎖的方式進行了改變,它不在方法上加鎖,它對程式碼塊進行加鎖,這樣的效率比飽漢式要高。具體程式碼如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
//1
synchronized (new Object()) {
//2
if(h == null){
h = new Demo();
}
}
}
return h;
}
}
複製程式碼
也許有人看了以上程式碼後會有疑問,要加上兩個if判斷幹嘛?加一個不行嗎,比如如下
public class Demo {
private static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
//1
synchronized (new Object()) {
//2
h = new Demo();
}
}
return h;
}
}
複製程式碼
這樣不也對進行例項化的時候加鎖了嗎?也可以保證執行緒安全啊!
看這個例子
- 執行緒A:嘿嘿!我已經走到了2,咦!竟然還有鎖,更好了,這初始化的好處我就要獨佔了,想想都雞凍,我要去初始化Demo類的例項了,啦啦啦。
- 執行緒B:哈哈!執行緒A那個SD,我都走到了1,它竟然還沒發現我,還想獨佔Demo的例項化,沒門!!臥槽,靜態被執行緒A那個SD上鎖了,哎等等吧!
- 執行緒B:咦!鎖的鑰匙又回來了,哎,沒希望了,希望能給我喝點湯。n秒後,B處於驚訝中,沒想到我還能初始化。哈哈哈。
為啥雙重會被認為是執行緒安全的。看這個例子
- 執行緒A:嘿嘿!我已經走到了2,咦!竟然還有鎖,更好了,這初始化的好處我就要獨佔了,想想都雞凍,我要去初始化Demo類的例項了,啦啦啦。
- 執行緒B:哈哈!執行緒A那個SD,我都走到了1,它竟然還沒發現我,還想獨佔Demo的例項化,沒門!!臥槽,靜態被執行緒A那個SD上鎖了,哎等等吧!
- 執行緒B:咦!鎖的鑰匙又回來了,哎,沒希望了,希望能給我喝點湯。n秒後,B處於崩潰中,沒想到竟然還有if(h == null)這個大門,我進不去了,嗚嗚嗚。
值得注意的是,該種產生單例的方式也會有執行緒安全的問題,學過java的都知道,java中在new物件的時候,並不是原子操作,它有以下三個大概步驟
- 分配記憶體空間
- 初始化物件
- 將記憶體地址賦給引用h
由於重排序原因(關於重排序的知識,可自行網上搜尋),可能在new物件時,第二步和第三步發生了交換,導致錯誤發生,理由是,此時的h是一個地址,但它
還沒完成初始化,如下例子
- 執行緒A:嘿嘿!我已經走到了2,咦!竟然還有鎖,更好了,這初始化的好處我就要獨佔了,想想都雞凍,我要去初始化Demo類的例項了,啦啦啦。
- 執行緒B:哈哈!執行緒A那個SD,我都走到了1,它竟然還沒發現我,還想獨佔Demo的例項化,沒門!!臥槽,靜態被執行緒A那個SD上鎖了,哎等等吧!
- 執行緒B:咦!鎖的鑰匙又回來了,哎,沒希望了,希望能給我喝點湯。n秒後,B處於崩潰中,沒想到竟然還有if(h == null)這個大門。
- 執行緒B:只能認命了,我只能訪問Demo物件玩玩,噗噗噗,竟然出錯了。。
其原因就如上述所說,發生了重排序導致的錯誤發生,當然,這個錯誤不一定經常發生。所有這時應該想的是怎麼防止重排序。
於是有人就想到了java中的volatile關鍵字來禁止程式碼的重排序,當然它還有保證可見性的功能,但不能和synchronized一樣還能保證原子性。
加了volatile關鍵字後,這樣才算真的執行緒安全,具體程式碼如下
public class Demo {
private volatile static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
if(h == null){
synchronized (new Object()) {
h = new Demo();
}
}
return h;
}
}
複製程式碼
4、靜態內部類
靜態內部類就是在該類的內部實現一個靜態內部類,內部類裡來實現該類的例項化,具體程式碼如下
public class Demo {
private volatile static Demo h = null;
private Demo(){
}
public static Demo getInstance(){
return InnerDemo.h;
}
//內部類
private static class InnerDemo{
private final static Demo h = new Demo();
}
}
複製程式碼
內部類的原理是利用了類載入器classloader機制來保證初始化h時只有一個執行緒,這樣也就保證了執行緒的安全性 。同時也不像餓汗式一樣,一進入該類就觸發例項初始化。內部類雖然是static,但只有在return InnerDemo.h時才會觸發該內部類的載入,也是懶載入的一種。
讀者福利:
分享免費學習資料
針對於還會準備免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料) 為什麼某些人會一直比你優秀,是因為他本身就很優秀還一直在持續努力變得更優秀,而你是不是還在滿足於現狀內心在竊喜!希望讀到這的您能點個小贊和關注下我,以後還會更新技術乾貨,謝謝您的支援!
資料領取方式:加入粉絲群963944895
,私信管理員即可免費領取