設計模式(五)最全單例模式

單例模式

關於單例模式的定義,我就直接引用Head First 了:單例模式確保一個類只有一個實例,並提供一個全局訪問點。 單例模式,按加載時機可以分爲:餓漢方式和懶漢方式。我們具體看下:

懶漢加載

這是最簡單的懶漢方式,但是卻不能保證線程安全。所以如果項目中涉及到多線程,應避免使用。懶漢式是延遲加載的,優點在於資源利用率高,但第一次調用時的初始化工作會導致性能延遲,以後每次獲取實例時也都要先判斷實例是否被初始化,造成些許效率損失。

public class Singleton {
    private static Singleton singleton;
    /**
     * 私有構造,只通過getInstance 對外提供實例
     */
    private Singleton() { }

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

如果我們爲上面代碼加上synchronized 修飾符呢?通過添加synchronized修飾符,確實能解決線程不安全問題,但這又會引入另外一個問題,那就是執行效率問題。每個線程每次執行 getInstance() 方法獲取類的實例時,都會進行同步,而事實上實例創建完成後,同步就變爲不必要的開銷了。

public class Singleton {
    private static Singleton singleton;
    
    private Singleton() { }

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

通過上面我們發現,要想保持同步,還要兼顧效率問題,那麼一個自然而然的思路就是:縮小同步區域的範圍了。這樣同步塊的方式就出來了。

public class Singleton {
    private static Singleton singleton;
    
    private Singleton() { }
    
    /**
     * 這樣存在線程問題。
     * 
     * <p>可能存在多個線程同時通過第一次檢查,
     * 導致創建了不同的對象。
     */
    public static Singleton getInstance() {
        // 第一次檢查,避免了不必要的同步
        if (singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

針對上面的情況,我們增加第二次檢查,來保證多線程問題。代碼見下:

public class Singleton {
	private static Singleton singleton;
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		if(Singleton == null){ // 第一次檢查,避免不必要同步
			synchronized (Singleton.class) {
				if(Singleton == null){ // 第二次檢查,線程安全
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

到這裏就高枕無憂了嗎?並不是,雙重檢查加鎖(DCL)會由於 Java 編譯器允許處理器亂序執行,所以會有 DCL 失效的問題。好在在JDK1.5 版本後,Java提供了volatile 關鍵字,來保證執行的順序,從而使單例起效。至於在JDK1.5版本以下,我們應避免使用該方式。雙重檢查加鎖 單例代碼見下:

public class Singleton {
	private volatile static Singleton singleton;
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		if(Singleton == null){
			synchronized (Singleton.class) {
				if(Singleton == null){
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

volatile關鍵字,是因爲volatile確保了應用中的可視性,如果你將一個域聲明爲volatile的,那麼只要對這個域產生了寫操作,那麼所有的讀操作就都可以看到這個修改。即便使用了本地緩存,情況也是如此,volatile域會立即被寫入到主存中,而讀取操作就發生在主存中(摘自java編程思想)。其實就是爲了防止編譯器優化指令執行,防止指令重排序問題,讓每次操作都是到主存中對同一份數據進行讀寫,防止發生問題。

餓漢加載

餓漢式天生是線程安全的。在只考慮一個類加載器的情況下,餓漢方式在系統運行起來裝載類的時候就進行初始化實例的操作,由 JVM 虛擬機來保證一個類的初始化方法在多線程環境中被正確加鎖和同步。餓漢式在類創建的同時就實例化了靜態對象,其資源已經初始化完成,所以第一次調用時更快,優勢在於速度和反應時間,但是不管此單例會不會被使用,在程序運行期間會一直佔據着一定的內存。

public class Singleton {
    public static final Singleton instance = new Singleton();
    
    private Singleton() { }
}

靜態代碼塊的餓漢式加載同樣能保證線程安全。

public class Singleton {
    private static final Singleton instance = null;
    
    static {
        instance = new Singleton();
    }
    
    private Singleton () { }
    
    public static Singleton getInstance() {
        return instance;
    }
}

這是我們常用的書寫形式,與上面兩種一樣。

public class Singleton {
    private static final Singleton instance = new Singleton();
    
    private Singleton() { }
    
    public static Singleton getInstance() {
        return instance;
    }
}
靜態內部類式加載

當未使用InterSingleton 靜態內部類時,InterSingleton 類並不會被初始化,只有在顯式調用 getInstance() 方法時,纔會裝載 InterSingleton 類,從而實例化對象,所以能達到延遲實例化的目的。同時在靜態初始化器中創建單件,也保證了線程的安全。此方式也避免了在JDK 版本低於 1.5 時雙重檢查加鎖方式的缺陷。

public class Singleton {
	
	private Singleton(){}

	public static Singleton getInstance() {
		return InterSingleton.singleton;
	}
	
	private static class InterSingleton {
		private static Singleton singleton = new Singleton();
	}
}
枚舉方式

枚舉不僅在創建實例的時候默認是線程安全的,而且在反序列化、反射、克隆時都可以自動防止重新創建新的對象。枚舉類也是在第一次訪問時才被實例化,屬於懶加載方式。由於枚舉是 JDK 1.5 才加入的特性,所以雙重加鎖檢查的方式一樣,對於版本有一定要求。

/**
 * 枚舉同Java中的普通Class一樣,
 * 也可以添加自己的屬性和方法。
 */
public enum Singleton {
    INSTANCE;
}
Map登記式單例

隨着項目越來越複雜,我們可能會需要同時管理多個不同業務的單例。這時我們就可以通過Map容器來統一管理這些單例,使用時,通過統一的接口來獲取相應單例。

public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String,Object>();
    
    private SingletonManager() { }
    
    private static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }
    
    public static Object getService(String key) {
        return objMap.get(key);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章