淺析Java併發中的單例模式

JavaDoop發表於2019-04-06

淺析Java併發中的單例模式

一、單例模式簡介

單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個例項。即一個類只有一個物件例項。在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,私信管理員即可免費領取

相關文章