你真的瞭解單例嗎
又到了一個老生常談的話題,單例模式,可能在面試時我們也經常會遇到,但是看似很簡單的問題,卻能看出一個人對單例理解的深度。要寫一個單例,首先需要讓構造器私有,還需要對外提供一個可以獲取單例的一個入口,通常我們可能會這樣寫:
第一種:
public class SingleTon {
private static SingleTon instance = new SingleTon();
private SingleTon(){}
public static SingleTon get(){
return instance;
}
}
這種方式簡單直接,例項隨著類載入而載入,很方便,但是卻不友好,有時候我們雖然載入了類,卻沒有使用該類例項的時候,會造成記憶體的浪費,不能達到懶載入的能力。所以我們可以改進成下面這樣:
第二種:
public class SingleTon {
private static SingleTon instance = null;
private SingleTon(){}
public static SingleTon get(){
if (instance == null){
instance =new SingleTon();
}
return instance;
}
}
這樣可以達到懶載入,需要的時候在初始化,但是如果在多執行緒的情況下是不完全的,那我們會這樣寫:
public class SingleTon { private static SingleTon instance = null; private SingleTon(){} public static synchronized SingleTon get(){ if (instance == null){ instance =new SingleTon(); } return instance; } }
雖然這樣安全了,但是鎖的粒度還是比較大,所以為了減小鎖的粒度我們還會這樣寫:
如果我們寫到這裡就以為很滿足了,那麼我只能說太天真了,看似一切完美,但是我們還是要問自己,這樣就絕對的會執行緒安全嗎?public class SingleTon { private static SingleTon instance = null; private SingleTon() { } public static SingleTon get() { if (instance == null) { synchronized (SingleTon.class) { if (instance == null) { instance = new SingleTon(); } } } return instance; } }
要回答這個問題,這就不得不說一說物件的建立過程和java虛擬機器的無序性。首先在我們new物件的時候,首先需要在方法區中去尋找該類的符號引用,如果找不到,說明類還沒有被載入進虛擬機器,所以需要通過類載入器先裝載該類,通過載入,驗證,準備,解析,初始化等操作,然後為物件在堆上開闢記憶體空間(1),物件初始化操作(2),然後在將棧上的引用指向該物件記憶體地址(3)。重點就在這,由於虛擬機器的無序性,可能會造成執行的順序並不是按照123進行的,也可能是按照132的執行順序,結果就是引用先指向物件地址,然後物件在進行初始化等操作,這是由於執行緒的可見性造成的,所以為了保證變數instance的執行緒之間的可見性,我們需要將instance變數進行volatile修飾來解決instance的可見性問題。(關於java虛擬機器的無序性和volatile的記憶體語義,涉及到了java記憶體模型的層面,這裡暫時不過多分析,後面會單獨進行講解)。
所以正確的寫法是這樣:
public class SingleTon {
private static volatile SingleTon instance = null;
private SingleTon() {
}
public static SingleTon get() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
}
return instance;
}
}
我們還可以改成成一個類,專門生成單例:
public abstract class Singleton<T> {
private volatile T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
那麼到此我們就可以滿足了嗎?當然不能。
這裡不可避免的需要對volatile進行解釋一下了,volatile在《深入理解java虛擬機器》中有一下幾層含義:
1,被volatile修飾的變數,保證了該變數對其他執行緒的可見性,;
2,禁止指令重排序,虛擬機器會通過插入很多讀寫記憶體屏障,來保證處理器不會亂序執行,但是也會造成編譯器不會對程式碼進行優化(java記憶體模型會最大限度的保證程式並行執行),對效率有一定影響。
那麼我們在不使用volatile的前提下如何優化呢,下面給出某大牛的寫法:
public class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon get() {
if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
SingleTon temp = null;
try {
temp = new SingleTon();
} catch (Exception e) {
}
if (temp != null)
instance = temp;
}
}
return instance;
}
}
看似無用的程式碼卻大有用處,try的存在虛擬機器無法優化temp是否為空,instance在賦值之前保證了物件已經初始化完成。看到這裡明顯感覺到水很深啊。
前面其實大概分為兩種,餓漢式和懶漢式,那有沒有既執行緒安全寫法簡單,又能懶載入呢?
第三種:
public class SingleTon {
private SingleTon() {
}
private static class SingleHolder {
private static SingleTon instance = new SingleTon();
}
public static SingleTon get() {
return SingleHolder.instance;
}
}
這裡我們通過靜態內部類來完成,是不是很妙,我們無需枷鎖,外部類的載入不會造成內部類同時載入的,只有呼叫了get方法時才會載入內部類,建立物件,集前兩種方法的優點於一身。
但是到這裡我們又要分析了,以上寫法到底完全不,如果通過反射或者反序列化還能保證是單例嗎?
當然不可能,在反射面前,一切都是小兒科了,這種寫法可阻止不了反射,反序列化也不行,你必須重寫readReslove方法,返回當前例項,不然就是多個例項了,
那到底有沒有絕對安全的單例啊,我們是不是都快絕望了,別急,放大招:
public enum SingleTon {
intstance;
}
是不是有點意外了,居然最簡單最安全的是列舉,至於列舉是如何做到反射和反序列化時依然安全的可以看連結:
好了,單例到此介紹完畢,看完這些你對單例模式真的瞭解了嗎?