單例模式的總體概述
單例模式,屬於創建型模式,《設計模式》一書對它做了定義:保證一個類僅有一個實例,並提供一個全局訪問點。
單例模式適用於無狀態的工具類、全局信息類等場景。例如日誌工具類,在系統中記錄日誌;假設我們需要統計網站的訪問次數,可以設置一個全局計數器。
單例模式的優勢有
- 在內存裏只有一個實例,減少了內存開銷;
- 可以避免對資源的多重佔用;
- 設置全局訪問點,嚴格控制訪問。
單例模式的研究重點大概有以下幾個:
- 構造私有,提供靜態輸出接口
- 線程安全,確保全局唯一
- 延遲初始化
- 防止反射攻擊
- 防止序列化破壞單例模式
多種實現方式與比較
線程安全的餓漢模式
public class HungrySingleton {
private final static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
也可以通過靜態代碼塊的形式實現。實現與靜態常量基本相同,只是把實例化過程放到了靜態代碼塊中。
private final static HungrySingleton2 instance;
static {
instance = new HungrySingleton2();
}
餓漢單例模式的特點有
- 實現簡單
- 線程安全
- 類加載時初始化實例
線程安全的懶漢單例模式
懶漢式用於解決延遲初始化問題,用到了才實例化。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
對於多線程來說,上面的實現存在競態條件:先檢查後執行,無法保證全局唯一。
通過給getInstance方法添加synchronized修飾,或者同步代碼塊形式很容易實現線程安全,保證全局唯一。
public synchronized static LazySingleton getInstance() {...}
或者
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
return instance;
}
然而代碼會對性能造成影響,在第一個實例創建成功後,我們便不再需要鎖。因此外層再對instance做一次空判斷,即雙重檢查鎖定。
雙重檢查鎖定和volatile優化
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
但是JVM即時編譯器中存在指令重排序優化。instance賦值語句包含了下面3個操作:
- 分配內存給對象
- 初始化對象
- 設置instance引用,指向剛分配的內存地址
程序執行時,步驟2和3可能出現重排序,導致instance先指向了內存地址,再初始化對象。其他線程外層校驗是instance不爲空,調用未完成初始化對象的方法會報空指針異常。
禁止指令重排序是volatile的兩大特性之一。使用volatile修飾instance,在賦值操作後加入內存柵欄,賦值之前的所有操作均可見。
靜態內部類實現延遲加載
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}
private static class InnerClass {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.INSTANCE;
}
}
靜態內部類的優點是:外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存。即當外部類第一次被加載時,並不需要去加載InnerClassr,只有當getInstance()方法第一次被調用時,纔會去初始化INSTANCE,第一次調用getInstance()方法會導致虛擬機加載InnerClass類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
在靜態內部類的初始化階段(class文件被加載後,被線程使用之前),執行類的初始化,JVM會獲取一個類Class對象的初始化鎖,鎖可以同步多個線程對一個類的初始化。因此,類初始化允許重排序,非構造線程是無法看到重排序的。
單元素枚舉實現單例模式
《Effective Java》推薦使用單元素枚舉類型實現Singleton,書中這樣描述“功能上與公有域方法類似,但更加簡潔無償地提供了序列化機制,絕對防止多次實例化”。
public enum EnumSingleton {
INSTANCE {
@Override
protected void print() {
System.out.println("使用枚舉構建單例模式");
}
};
protected abstract void print();
public static EnumSingleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.getInstance();
instance.print();
}
}
反編譯代碼
public abstract class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
protected abstract void print();
public static EnumSingleton getInstance()
{
return INSTANCE;
}
public static void main(String args[])
{
EnumSingleton instance = getInstance();
instance.print();
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0) {
protected void print()
{
System.out.println("enum singleton");
}
};
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
從反編譯出的代碼中可以看出,EnumInstance類在加載時,就把INSTANCE屬性初始化好了,和餓漢模式類似。
- 類final–不能被繼承
- 構造器私有–不能外部實例化
- 類變量是靜態的–類加載初始化
【注】在EnumInstance類中,不定義print()方法的話,class反編譯後是final類型的。
各種實現方式的選取
最好的實現方式是枚舉,可以避免反射和序列化對單例模式的破壞;不能使用線程不安全的實現方式;如果程序一開始要加載的資源太多,就應該選取懶加載;餓漢單例模式在對象創建需要配置文件時不適用。
下一小節:《如何避免反射和序列化破壞單例模式》