1. 程式人生 > >單例模式:二-懶漢模式(Lazy)

單例模式:二-懶漢模式(Lazy)

原文: Gerrard_Feng

二:2-懶漢模式(Lazy)
  
思想:相比於餓漢模式,懶漢模式實際中的應用更多,因為在系統中,“被用到時再初始化”是更佳的解決方案。

  設計思想與餓漢模式類似,同樣是持有一個自身的引用,只是將 new 的動作延遲到 getinstance() 方法中執行。

複製程式碼
public final class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
        if (instance != null) {
            throw new IllegalStateException();

        }
    }

    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
複製程式碼
 

反射能否打破單例?
  對於 LazySingleton,這是個很有趣的問題,雖然我們在私有構造器中增加了 instance==null 的判斷,但是由於延遲載入的原因,使得它無法完美地規避反射的入侵。

  這涉及到了反射入侵和 getInstance() 方法呼叫順序的問題。

  如果在呼叫 getInstance() 方法之前進行反射入侵,那麼就會打破單例,反之,可以保證單例。

    public class LazySingletonTest {

    @Test
    public void testReflectSuccess() throws Exception {
        Constructor<?> constructor = LazySingleton1.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        LazySingleton1 singleton1 = (LazySingleton1) constructor.newInstance();
        LazySingleton1 singleton2 = LazySingleton1.getInstance();
        Assert.assertNotSame(singleton1, singleton2);
    }

    @Test
    public void testReflectFailure() throws Exception {
        LazySingleton1 singleton1 = LazySingleton1.getInstance();
        Constructor<?> constructor = LazySingleton1.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        try {
            LazySingleton1 singleton2 = (LazySingleton1) constructor.newInstance();
            Assert.fail();
        } catch (Exception e) {
            // Do nothing, test pass
        }
    }
    }
 

為什麼是 synchronized 方法?
  因為是延遲載入,考慮到多執行緒情況,需要對方法同步。

同步方法帶來的效能問題?
  可以使用 synchronized 程式碼塊 + Double-check Locking + volatile 關鍵字,對 LazySingleton 進行深一步優化:

懶漢模式的實現,以及實現的原因。

Step1:基礎的懶漢模式

public class LazySingleton {

private static LazySingleton instance = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
    if (instance == null) {
        instance = new LazySingleton();
    }
    return instance;
}

} 基礎的懶漢模式保證了在呼叫 getInstance() 方法的時候才第一次初始化單例物件。

但是這麼做無法保證在多執行緒環境下只建立一個物件。

顯然,假設有多個執行緒同時呼叫 getInstance() 方法,在第一個執行緒執行完畢之前,會有多個 LazyInstance 物件被建立。

Step2:為 getInstance() 方法加上同步鎖

public class LazySingleton {

private static LazySingleton instance = null;

private LazySingleton() {
}

public synchronized static LazySingleton getInstance() {
    if (instance == null) {
        instance = new LazySingleton();
    }
    return instance;
}

} 通過簡單地在方法上加上同步鎖,可以保證同時只有一個執行緒呼叫這個靜態方法,從而保證在多執行緒環境下的單例。

然而這麼做有明顯的 效能 隱患。

假設有多個執行緒想要獲取 instance,無論此時物件是否已經被建立,都要頻繁地獲取鎖,釋放鎖。這種做法很影響效率。

Step3:在 getInstance() 方法內部增加同步程式碼塊

public class LazySingleton {

private static LazySingleton instance = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
    if (instance == null) {
        synchronized (LazySingleton.class) {
            instance = new LazySingleton();
        }
    }
    return instance;
}

} 既然在方法上加同步鎖不合適,那麼就在方法內部增加同步程式碼塊。

在判斷 instance == null 之後,增加的同步程式碼塊就不會產生 performance 問題,因為之後的訪問會直接 return,不會進入同步程式碼塊。

但是這麼做,不能完整地保證單例。

參照 Step1,假設有多執行緒呼叫,且都通過了 instance == null 的判斷,那麼一樣會有多個 LazySingleton 物件被建立。

Step4:使用 Double-Checked Locking

public class LazySingleton {

private static LazySingleton instance = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
    if (instance == null) {
        synchronized (LazySingleton.class) {
            if (instance == null) {
                instance = new LazySingleton();
            }
        }
    }
    return instance;
}

} 通過增加雙重判斷( Double-Checked Locking),以及同步程式碼塊,就可以避免 Step3 中可能出現的隱患。

但是 Double-Checked Locking 雖然能夠保證單例的建立,但是在多執行緒的情況下可能出現某個執行緒使用建立不完全的物件的情況。

Step5:使用 volatile 關鍵字修飾字段 instance

public class LazySingleton {

private static volatile LazySingleton instance = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
    if (instance == null) {
        synchronized (LazySingleton.class) {
            if (instance == null) {
                instance = new LazySingleton();
            }
        }
    }
    return instance;
}

}

參考文件:The "Double-Checked Locking is Broken" Declaration

如果不適應英文描述,ImportNew 對這篇文件進行了翻譯:可以不要再使用Double-Checked Locking了

這裡面講述了 Double-Checked Locking 在懶漢模式下可能出現的問題。

主要問題在於 Java 指令重排。

當 Java 程式碼被編譯器翻譯成位元組碼被儲存在 JVM 時,為了提高效能,編譯器會對這些操作指令進行指令重排。

也就是說,程式碼在計算機上執行的順序,會被打亂。

返回到本例的問題,懶漢模式最關鍵的2個操作:

1.在 heap 中建立一個 LazyInstance 物件。 2.為欄位 instance 賦值。 假設操作1在操作2之前被執行,那麼程式碼就沒有問題。

反之若操作2在操作1之前被執行,如果不能保證建立 LazyInstance 物件的過程是原子的,那麼程式碼還是會出現問題,因為 instance 指向了一個沒有被建立完全的物件。

事實上,引用型別和64位型別(long 和 double)都不能被原子地讀寫。

解決方案是通過 volatile 關鍵字來禁止指令重排(這是 volatile 的兩個作用之一,另一個作用是保證共享變數的可見性,這裡不深入展開)

 
 優勢?劣勢?
優勢:延遲載入。

劣勢:不能完全遮蔽反射入侵,而且程